feat: 支持OpenAI/Anthropic双API提供者切换,Banner显示URL和模型
通过 claude-code.provider 配置切换API提供者: - openai(默认): 使用 OpenAI 兼容 API,支持各种代理服务 - anthropic: 使用 Anthropic 原生 API 配置方式: - 环境变量: CLAUDE_CODE_PROVIDER=openai|anthropic - application.yml: claude-code.provider Banner 增强: - 启动时显示当前 Provider、Model、API URL - Scanner降级模式也显示同样信息 启动脚本更新: - run.ps1/run.bat 添加完整的双provider配置说明 Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
This commit is contained in:
@@ -12,7 +12,18 @@ REM === 抑制 Maven JVM 的 JDK25 兼容性警告 ===
|
|||||||
set MAVEN_OPTS=--enable-native-access=ALL-UNNAMED --sun-misc-unsafe-memory-access=allow
|
set MAVEN_OPTS=--enable-native-access=ALL-UNNAMED --sun-misc-unsafe-memory-access=allow
|
||||||
|
|
||||||
REM === AI API 配置(按需修改) ===
|
REM === AI API 配置(按需修改) ===
|
||||||
|
REM 选择 API 提供者:openai(默认)或 anthropic
|
||||||
|
REM set CLAUDE_CODE_PROVIDER=openai
|
||||||
|
REM set CLAUDE_CODE_PROVIDER=anthropic
|
||||||
|
|
||||||
|
REM OpenAI 兼容 API 配置(默认)
|
||||||
|
REM set AI_API_KEY=your-api-key-here
|
||||||
|
REM set AI_BASE_URL=https://api.openai.com
|
||||||
|
REM set AI_OPENAI_MODEL=gpt-4o
|
||||||
|
|
||||||
|
REM Anthropic 原生 API 配置
|
||||||
REM set ANTHROPIC_API_KEY=your-api-key-here
|
REM set ANTHROPIC_API_KEY=your-api-key-here
|
||||||
|
REM set ANTHROPIC_BASE_URL=https://api.anthropic.com
|
||||||
REM set AI_MODEL=claude-sonnet-4-20250514
|
REM set AI_MODEL=claude-sonnet-4-20250514
|
||||||
|
|
||||||
REM === 设置控制台 UTF-8 编码(支持 emoji 等字符) ===
|
REM === 设置控制台 UTF-8 编码(支持 emoji 等字符) ===
|
||||||
|
|||||||
@@ -11,7 +11,18 @@ $env:Path = "D:\Dev\jdk-25\bin;$env:Path"
|
|||||||
$env:MAVEN_OPTS = "--enable-native-access=ALL-UNNAMED --sun-misc-unsafe-memory-access=allow"
|
$env:MAVEN_OPTS = "--enable-native-access=ALL-UNNAMED --sun-misc-unsafe-memory-access=allow"
|
||||||
|
|
||||||
# === AI API 配置(按需修改) ===
|
# === AI API 配置(按需修改) ===
|
||||||
|
# 选择 API 提供者:openai(默认)或 anthropic
|
||||||
|
# $env:CLAUDE_CODE_PROVIDER = "openai" # 使用 OpenAI 兼容 API(支持代理)
|
||||||
|
# $env:CLAUDE_CODE_PROVIDER = "anthropic" # 使用 Anthropic 原生 API
|
||||||
|
|
||||||
|
# OpenAI 兼容 API 配置(默认)
|
||||||
|
# $env:AI_API_KEY = "your-api-key-here"
|
||||||
|
# $env:AI_BASE_URL = "https://api.openai.com"
|
||||||
|
# $env:AI_OPENAI_MODEL = "gpt-4o"
|
||||||
|
|
||||||
|
# Anthropic 原生 API 配置
|
||||||
# $env:ANTHROPIC_API_KEY = "your-api-key-here"
|
# $env:ANTHROPIC_API_KEY = "your-api-key-here"
|
||||||
|
# $env:ANTHROPIC_BASE_URL = "https://api.anthropic.com"
|
||||||
# $env:AI_MODEL = "claude-sonnet-4-20250514"
|
# $env:AI_MODEL = "claude-sonnet-4-20250514"
|
||||||
|
|
||||||
# === 设置控制台 UTF-8 编码(支持 emoji 等字符) ===
|
# === 设置控制台 UTF-8 编码(支持 emoji 等字符) ===
|
||||||
|
|||||||
@@ -12,21 +12,31 @@ import com.claudecode.repl.ReplSession;
|
|||||||
import com.claudecode.tool.ToolContext;
|
import com.claudecode.tool.ToolContext;
|
||||||
import com.claudecode.tool.ToolRegistry;
|
import com.claudecode.tool.ToolRegistry;
|
||||||
import com.claudecode.tool.impl.*;
|
import com.claudecode.tool.impl.*;
|
||||||
|
import org.slf4j.Logger;
|
||||||
|
import org.slf4j.LoggerFactory;
|
||||||
import org.springframework.ai.chat.model.ChatModel;
|
import org.springframework.ai.chat.model.ChatModel;
|
||||||
import org.springframework.beans.factory.annotation.Qualifier;
|
import org.springframework.beans.factory.annotation.Qualifier;
|
||||||
|
import org.springframework.beans.factory.annotation.Value;
|
||||||
import org.springframework.context.annotation.Bean;
|
import org.springframework.context.annotation.Bean;
|
||||||
import org.springframework.context.annotation.Configuration;
|
import org.springframework.context.annotation.Configuration;
|
||||||
|
|
||||||
import java.nio.file.Path;
|
import java.nio.file.Path;
|
||||||
|
import java.util.Map;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 应用配置类 —— Spring Bean 装配。
|
* 应用配置类 —— Spring Bean 装配。
|
||||||
* <p>
|
* <p>
|
||||||
* 集中管理所有组件的创建和依赖注入。
|
* 集中管理所有组件的创建和依赖注入。
|
||||||
|
* 通过 claude-code.provider 配置切换 API 提供者(openai / anthropic)。
|
||||||
*/
|
*/
|
||||||
@Configuration
|
@Configuration
|
||||||
public class AppConfig {
|
public class AppConfig {
|
||||||
|
|
||||||
|
private static final Logger log = LoggerFactory.getLogger(AppConfig.class);
|
||||||
|
|
||||||
|
@Value("${claude-code.provider:openai}")
|
||||||
|
private String provider;
|
||||||
|
|
||||||
@Bean
|
@Bean
|
||||||
public ToolContext toolContext() {
|
public ToolContext toolContext() {
|
||||||
return ToolContext.defaultContext();
|
return ToolContext.defaultContext();
|
||||||
@@ -69,11 +79,45 @@ public class AppConfig {
|
|||||||
return registry;
|
return registry;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 根据 claude-code.provider 配置选择 ChatModel。
|
||||||
|
* - "anthropic" → 使用 anthropicChatModel
|
||||||
|
* - "openai"(默认)→ 使用 openAiChatModel
|
||||||
|
*/
|
||||||
@Bean
|
@Bean
|
||||||
public TokenTracker tokenTracker() {
|
public ChatModel activeChatModel(
|
||||||
|
@Qualifier("openAiChatModel") ChatModel openAiModel,
|
||||||
|
@Qualifier("anthropicChatModel") ChatModel anthropicModel) {
|
||||||
|
|
||||||
|
if ("anthropic".equalsIgnoreCase(provider)) {
|
||||||
|
log.info("使用 Anthropic 原生 API");
|
||||||
|
return anthropicModel;
|
||||||
|
} else {
|
||||||
|
log.info("使用 OpenAI 兼容 API");
|
||||||
|
return openAiModel;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Bean
|
||||||
|
public ProviderInfo providerInfo() {
|
||||||
|
String baseUrl;
|
||||||
|
String model;
|
||||||
|
|
||||||
|
if ("anthropic".equalsIgnoreCase(provider)) {
|
||||||
|
baseUrl = System.getenv().getOrDefault("ANTHROPIC_BASE_URL", "https://api.anthropic.com");
|
||||||
|
model = System.getenv().getOrDefault("AI_MODEL", "claude-sonnet-4-20250514");
|
||||||
|
} else {
|
||||||
|
baseUrl = System.getenv().getOrDefault("AI_BASE_URL", "https://api.openai.com");
|
||||||
|
model = System.getenv().getOrDefault("AI_OPENAI_MODEL", "gpt-4o");
|
||||||
|
}
|
||||||
|
|
||||||
|
return new ProviderInfo(provider, baseUrl, model);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Bean
|
||||||
|
public TokenTracker tokenTracker(ProviderInfo info) {
|
||||||
TokenTracker tracker = new TokenTracker();
|
TokenTracker tracker = new TokenTracker();
|
||||||
String model = System.getenv().getOrDefault("AI_MODEL", "claude-sonnet-4-20250514");
|
tracker.setModel(info.model());
|
||||||
tracker.setModel(model);
|
|
||||||
return tracker;
|
return tracker;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -81,16 +125,13 @@ public class AppConfig {
|
|||||||
public String systemPrompt() {
|
public String systemPrompt() {
|
||||||
Path projectDir = Path.of(System.getProperty("user.dir"));
|
Path projectDir = Path.of(System.getProperty("user.dir"));
|
||||||
|
|
||||||
// 加载 CLAUDE.md
|
|
||||||
ClaudeMdLoader claudeLoader = new ClaudeMdLoader(projectDir);
|
ClaudeMdLoader claudeLoader = new ClaudeMdLoader(projectDir);
|
||||||
String claudeMd = claudeLoader.load();
|
String claudeMd = claudeLoader.load();
|
||||||
|
|
||||||
// 加载 Skills
|
|
||||||
SkillLoader skillLoader = new SkillLoader(projectDir);
|
SkillLoader skillLoader = new SkillLoader(projectDir);
|
||||||
skillLoader.loadAll();
|
skillLoader.loadAll();
|
||||||
String skillsSummary = skillLoader.buildSkillsSummary();
|
String skillsSummary = skillLoader.buildSkillsSummary();
|
||||||
|
|
||||||
// 收集 Git 上下文
|
|
||||||
GitContext gitContext = new GitContext(projectDir).collect();
|
GitContext gitContext = new GitContext(projectDir).collect();
|
||||||
String gitSummary = gitContext.buildSummary();
|
String gitSummary = gitContext.buildSummary();
|
||||||
|
|
||||||
@@ -102,14 +143,14 @@ public class AppConfig {
|
|||||||
}
|
}
|
||||||
|
|
||||||
@Bean
|
@Bean
|
||||||
public AgentLoop agentLoop(@Qualifier("anthropicChatModel") ChatModel chatModel, ToolRegistry toolRegistry,
|
public AgentLoop agentLoop(ChatModel activeChatModel, ToolRegistry toolRegistry,
|
||||||
ToolContext toolContext, String systemPrompt, TokenTracker tokenTracker) {
|
ToolContext toolContext, String systemPrompt, TokenTracker tokenTracker) {
|
||||||
AgentLoop mainLoop = new AgentLoop(chatModel, toolRegistry, toolContext, systemPrompt, tokenTracker);
|
AgentLoop mainLoop = new AgentLoop(activeChatModel, toolRegistry, toolContext, systemPrompt, tokenTracker);
|
||||||
|
|
||||||
// 注册子 Agent 工厂到 ToolContext,使 AgentTool 能创建独立的 AgentLoop
|
// 注册子 Agent 工厂
|
||||||
toolContext.set(AgentTool.AGENT_FACTORY_KEY,
|
toolContext.set(AgentTool.AGENT_FACTORY_KEY,
|
||||||
(java.util.function.Function<String, String>) prompt -> {
|
(java.util.function.Function<String, String>) prompt -> {
|
||||||
AgentLoop subLoop = new AgentLoop(chatModel, toolRegistry, toolContext, systemPrompt);
|
AgentLoop subLoop = new AgentLoop(activeChatModel, toolRegistry, toolContext, systemPrompt);
|
||||||
return subLoop.run(prompt);
|
return subLoop.run(prompt);
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -118,7 +159,11 @@ public class AppConfig {
|
|||||||
|
|
||||||
@Bean
|
@Bean
|
||||||
public ReplSession replSession(AgentLoop agentLoop, ToolRegistry toolRegistry,
|
public ReplSession replSession(AgentLoop agentLoop, ToolRegistry toolRegistry,
|
||||||
CommandRegistry commandRegistry) {
|
CommandRegistry commandRegistry, ProviderInfo providerInfo) {
|
||||||
return new ReplSession(agentLoop, toolRegistry, commandRegistry);
|
return new ReplSession(agentLoop, toolRegistry, commandRegistry, providerInfo);
|
||||||
|
}
|
||||||
|
|
||||||
|
/** API 提供者信息,供 Banner 和命令显示 */
|
||||||
|
public record ProviderInfo(String provider, String baseUrl, String model) {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
package com.claudecode.repl;
|
package com.claudecode.repl;
|
||||||
|
|
||||||
|
import com.claudecode.config.AppConfig.ProviderInfo;
|
||||||
import com.claudecode.command.CommandContext;
|
import com.claudecode.command.CommandContext;
|
||||||
import com.claudecode.command.CommandRegistry;
|
import com.claudecode.command.CommandRegistry;
|
||||||
import com.claudecode.console.*;
|
import com.claudecode.console.*;
|
||||||
@@ -40,6 +41,7 @@ public class ReplSession {
|
|||||||
private final AgentLoop agentLoop;
|
private final AgentLoop agentLoop;
|
||||||
private final ToolRegistry toolRegistry;
|
private final ToolRegistry toolRegistry;
|
||||||
private final CommandRegistry commandRegistry;
|
private final CommandRegistry commandRegistry;
|
||||||
|
private final ProviderInfo providerInfo;
|
||||||
private final PrintStream out;
|
private final PrintStream out;
|
||||||
private final ToolStatusRenderer toolStatusRenderer;
|
private final ToolStatusRenderer toolStatusRenderer;
|
||||||
private final MarkdownRenderer markdownRenderer;
|
private final MarkdownRenderer markdownRenderer;
|
||||||
@@ -49,10 +51,12 @@ public class ReplSession {
|
|||||||
|
|
||||||
public ReplSession(AgentLoop agentLoop,
|
public ReplSession(AgentLoop agentLoop,
|
||||||
ToolRegistry toolRegistry,
|
ToolRegistry toolRegistry,
|
||||||
CommandRegistry commandRegistry) {
|
CommandRegistry commandRegistry,
|
||||||
|
ProviderInfo providerInfo) {
|
||||||
this.agentLoop = agentLoop;
|
this.agentLoop = agentLoop;
|
||||||
this.toolRegistry = toolRegistry;
|
this.toolRegistry = toolRegistry;
|
||||||
this.commandRegistry = commandRegistry;
|
this.commandRegistry = commandRegistry;
|
||||||
|
this.providerInfo = providerInfo;
|
||||||
// 强制使用 UTF-8 编码输出,确保 emoji 等 Unicode 字符在 Windows 终端正常显示
|
// 强制使用 UTF-8 编码输出,确保 emoji 等 Unicode 字符在 Windows 终端正常显示
|
||||||
this.out = new PrintStream(System.out, true, StandardCharsets.UTF_8);
|
this.out = new PrintStream(System.out, true, StandardCharsets.UTF_8);
|
||||||
this.toolStatusRenderer = new ToolStatusRenderer(out);
|
this.toolStatusRenderer = new ToolStatusRenderer(out);
|
||||||
@@ -163,7 +167,13 @@ public class ReplSession {
|
|||||||
/** 打印启动 Banner(JLine 模式) */
|
/** 打印启动 Banner(JLine 模式) */
|
||||||
private void printBanner(Terminal terminal) {
|
private void printBanner(Terminal terminal) {
|
||||||
BannerPrinter.printCompact(out);
|
BannerPrinter.printCompact(out);
|
||||||
out.println(AnsiStyle.dim(" Working directory: " + System.getProperty("user.dir")));
|
|
||||||
|
// 显示 API 提供者、模型和 URL
|
||||||
|
out.println(AnsiStyle.dim(" Provider: ") + AnsiStyle.cyan(providerInfo.provider().toUpperCase())
|
||||||
|
+ AnsiStyle.dim(" Model: ") + AnsiStyle.cyan(providerInfo.model()));
|
||||||
|
out.println(AnsiStyle.dim(" API URL: ") + AnsiStyle.cyan(providerInfo.baseUrl()));
|
||||||
|
|
||||||
|
out.println(AnsiStyle.dim(" Work Dir: " + System.getProperty("user.dir")));
|
||||||
out.println(AnsiStyle.dim(" Tools: " + toolRegistry.size() + " registered"));
|
out.println(AnsiStyle.dim(" Tools: " + toolRegistry.size() + " registered"));
|
||||||
|
|
||||||
boolean isDumb = "dumb".equals(terminal.getType());
|
boolean isDumb = "dumb".equals(terminal.getType());
|
||||||
@@ -188,7 +198,10 @@ public class ReplSession {
|
|||||||
|
|
||||||
private void startWithScanner() {
|
private void startWithScanner() {
|
||||||
BannerPrinter.printCompact(out);
|
BannerPrinter.printCompact(out);
|
||||||
out.println(AnsiStyle.dim(" Working directory: " + System.getProperty("user.dir")));
|
out.println(AnsiStyle.dim(" Provider: ") + AnsiStyle.cyan(providerInfo.provider().toUpperCase())
|
||||||
|
+ AnsiStyle.dim(" Model: ") + AnsiStyle.cyan(providerInfo.model()));
|
||||||
|
out.println(AnsiStyle.dim(" API URL: ") + AnsiStyle.cyan(providerInfo.baseUrl()));
|
||||||
|
out.println(AnsiStyle.dim(" Work Dir: " + System.getProperty("user.dir")));
|
||||||
out.println(AnsiStyle.dim(" Tools: " + toolRegistry.size() + " registered"));
|
out.println(AnsiStyle.dim(" Tools: " + toolRegistry.size() + " registered"));
|
||||||
out.println(AnsiStyle.dim(" Mode: Scanner (basic input)"));
|
out.println(AnsiStyle.dim(" Mode: Scanner (basic input)"));
|
||||||
out.println();
|
out.println();
|
||||||
|
|||||||
@@ -1,21 +1,25 @@
|
|||||||
spring:
|
spring:
|
||||||
# 主配置:使用 Anthropic 原生 API
|
|
||||||
ai:
|
ai:
|
||||||
|
# === Anthropic 原生 API 配置 ===
|
||||||
|
# 启用时设置 claude-code.provider=anthropic
|
||||||
anthropic:
|
anthropic:
|
||||||
api-key: ${ANTHROPIC_API_KEY:}
|
api-key: ${ANTHROPIC_API_KEY:}
|
||||||
base-url: ${ANTHROPIC_BASE_URL:https://open.bigmodel.cn/api/anthropic}
|
base-url: ${ANTHROPIC_BASE_URL:https://api.anthropic.com}
|
||||||
chat:
|
chat:
|
||||||
options:
|
options:
|
||||||
model: ${AI_MODEL:claude-sonnet-4}
|
model: ${AI_MODEL:claude-sonnet-4-20250514}
|
||||||
max-tokens: ${AI_MAX_TOKENS:8096}
|
max-tokens: ${AI_MAX_TOKENS:8096}
|
||||||
temperature: 0.7
|
temperature: 0.7
|
||||||
# 备选:兼容 OpenAI 格式的 API(如自建代理)
|
|
||||||
|
# === OpenAI 兼容 API 配置 ===
|
||||||
|
# 启用时设置 claude-code.provider=openai(默认)
|
||||||
|
# 支持 OpenAI、Azure OpenAI、兼容 API 代理等
|
||||||
openai:
|
openai:
|
||||||
api-key: ${AI_API_KEY:}
|
api-key: ${AI_API_KEY:}
|
||||||
base-url: ${AI_BASE_URL:https://api.openai.com}
|
base-url: ${AI_BASE_URL:https://api.openai.com}
|
||||||
chat:
|
chat:
|
||||||
options:
|
options:
|
||||||
model: ${AI_OPENAI_MODEL:}
|
model: ${AI_OPENAI_MODEL:gpt-4o}
|
||||||
|
|
||||||
# 不启动 Web 服务器(纯 CLI 模式)
|
# 不启动 Web 服务器(纯 CLI 模式)
|
||||||
main:
|
main:
|
||||||
@@ -27,6 +31,11 @@ spring:
|
|||||||
virtual:
|
virtual:
|
||||||
enabled: true
|
enabled: true
|
||||||
|
|
||||||
|
# === Claude Code Java 自定义配置 ===
|
||||||
|
claude-code:
|
||||||
|
# API 提供者:openai 或 anthropic
|
||||||
|
provider: ${CLAUDE_CODE_PROVIDER:openai}
|
||||||
|
|
||||||
# 日志:CLI 模式下只显示 WARN 及以上,保持控制台干净
|
# 日志:CLI 模式下只显示 WARN 及以上,保持控制台干净
|
||||||
logging:
|
logging:
|
||||||
level:
|
level:
|
||||||
|
|||||||
Reference in New Issue
Block a user