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
|
||||
|
||||
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_BASE_URL=https://api.anthropic.com
|
||||
REM set AI_MODEL=claude-sonnet-4-20250514
|
||||
|
||||
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"
|
||||
|
||||
# === 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_BASE_URL = "https://api.anthropic.com"
|
||||
# $env:AI_MODEL = "claude-sonnet-4-20250514"
|
||||
|
||||
# === 设置控制台 UTF-8 编码(支持 emoji 等字符) ===
|
||||
|
||||
@@ -12,21 +12,31 @@ import com.claudecode.repl.ReplSession;
|
||||
import com.claudecode.tool.ToolContext;
|
||||
import com.claudecode.tool.ToolRegistry;
|
||||
import com.claudecode.tool.impl.*;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
import org.springframework.ai.chat.model.ChatModel;
|
||||
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.Configuration;
|
||||
|
||||
import java.nio.file.Path;
|
||||
import java.util.Map;
|
||||
|
||||
/**
|
||||
* 应用配置类 —— Spring Bean 装配。
|
||||
* <p>
|
||||
* 集中管理所有组件的创建和依赖注入。
|
||||
* 通过 claude-code.provider 配置切换 API 提供者(openai / anthropic)。
|
||||
*/
|
||||
@Configuration
|
||||
public class AppConfig {
|
||||
|
||||
private static final Logger log = LoggerFactory.getLogger(AppConfig.class);
|
||||
|
||||
@Value("${claude-code.provider:openai}")
|
||||
private String provider;
|
||||
|
||||
@Bean
|
||||
public ToolContext toolContext() {
|
||||
return ToolContext.defaultContext();
|
||||
@@ -69,11 +79,45 @@ public class AppConfig {
|
||||
return registry;
|
||||
}
|
||||
|
||||
/**
|
||||
* 根据 claude-code.provider 配置选择 ChatModel。
|
||||
* - "anthropic" → 使用 anthropicChatModel
|
||||
* - "openai"(默认)→ 使用 openAiChatModel
|
||||
*/
|
||||
@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();
|
||||
String model = System.getenv().getOrDefault("AI_MODEL", "claude-sonnet-4-20250514");
|
||||
tracker.setModel(model);
|
||||
tracker.setModel(info.model());
|
||||
return tracker;
|
||||
}
|
||||
|
||||
@@ -81,16 +125,13 @@ public class AppConfig {
|
||||
public String systemPrompt() {
|
||||
Path projectDir = Path.of(System.getProperty("user.dir"));
|
||||
|
||||
// 加载 CLAUDE.md
|
||||
ClaudeMdLoader claudeLoader = new ClaudeMdLoader(projectDir);
|
||||
String claudeMd = claudeLoader.load();
|
||||
|
||||
// 加载 Skills
|
||||
SkillLoader skillLoader = new SkillLoader(projectDir);
|
||||
skillLoader.loadAll();
|
||||
String skillsSummary = skillLoader.buildSkillsSummary();
|
||||
|
||||
// 收集 Git 上下文
|
||||
GitContext gitContext = new GitContext(projectDir).collect();
|
||||
String gitSummary = gitContext.buildSummary();
|
||||
|
||||
@@ -102,14 +143,14 @@ public class AppConfig {
|
||||
}
|
||||
|
||||
@Bean
|
||||
public AgentLoop agentLoop(@Qualifier("anthropicChatModel") ChatModel chatModel, ToolRegistry toolRegistry,
|
||||
public AgentLoop agentLoop(ChatModel activeChatModel, ToolRegistry toolRegistry,
|
||||
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,
|
||||
(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);
|
||||
});
|
||||
|
||||
@@ -118,7 +159,11 @@ public class AppConfig {
|
||||
|
||||
@Bean
|
||||
public ReplSession replSession(AgentLoop agentLoop, ToolRegistry toolRegistry,
|
||||
CommandRegistry commandRegistry) {
|
||||
return new ReplSession(agentLoop, toolRegistry, commandRegistry);
|
||||
CommandRegistry commandRegistry, ProviderInfo providerInfo) {
|
||||
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;
|
||||
|
||||
import com.claudecode.config.AppConfig.ProviderInfo;
|
||||
import com.claudecode.command.CommandContext;
|
||||
import com.claudecode.command.CommandRegistry;
|
||||
import com.claudecode.console.*;
|
||||
@@ -40,6 +41,7 @@ public class ReplSession {
|
||||
private final AgentLoop agentLoop;
|
||||
private final ToolRegistry toolRegistry;
|
||||
private final CommandRegistry commandRegistry;
|
||||
private final ProviderInfo providerInfo;
|
||||
private final PrintStream out;
|
||||
private final ToolStatusRenderer toolStatusRenderer;
|
||||
private final MarkdownRenderer markdownRenderer;
|
||||
@@ -49,10 +51,12 @@ public class ReplSession {
|
||||
|
||||
public ReplSession(AgentLoop agentLoop,
|
||||
ToolRegistry toolRegistry,
|
||||
CommandRegistry commandRegistry) {
|
||||
CommandRegistry commandRegistry,
|
||||
ProviderInfo providerInfo) {
|
||||
this.agentLoop = agentLoop;
|
||||
this.toolRegistry = toolRegistry;
|
||||
this.commandRegistry = commandRegistry;
|
||||
this.providerInfo = providerInfo;
|
||||
// 强制使用 UTF-8 编码输出,确保 emoji 等 Unicode 字符在 Windows 终端正常显示
|
||||
this.out = new PrintStream(System.out, true, StandardCharsets.UTF_8);
|
||||
this.toolStatusRenderer = new ToolStatusRenderer(out);
|
||||
@@ -163,7 +167,13 @@ public class ReplSession {
|
||||
/** 打印启动 Banner(JLine 模式) */
|
||||
private void printBanner(Terminal terminal) {
|
||||
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"));
|
||||
|
||||
boolean isDumb = "dumb".equals(terminal.getType());
|
||||
@@ -188,7 +198,10 @@ public class ReplSession {
|
||||
|
||||
private void startWithScanner() {
|
||||
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(" Mode: Scanner (basic input)"));
|
||||
out.println();
|
||||
|
||||
@@ -1,21 +1,25 @@
|
||||
spring:
|
||||
# 主配置:使用 Anthropic 原生 API
|
||||
ai:
|
||||
# === Anthropic 原生 API 配置 ===
|
||||
# 启用时设置 claude-code.provider=anthropic
|
||||
anthropic:
|
||||
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:
|
||||
options:
|
||||
model: ${AI_MODEL:claude-sonnet-4}
|
||||
model: ${AI_MODEL:claude-sonnet-4-20250514}
|
||||
max-tokens: ${AI_MAX_TOKENS:8096}
|
||||
temperature: 0.7
|
||||
# 备选:兼容 OpenAI 格式的 API(如自建代理)
|
||||
|
||||
# === OpenAI 兼容 API 配置 ===
|
||||
# 启用时设置 claude-code.provider=openai(默认)
|
||||
# 支持 OpenAI、Azure OpenAI、兼容 API 代理等
|
||||
openai:
|
||||
api-key: ${AI_API_KEY:}
|
||||
base-url: ${AI_BASE_URL:https://api.openai.com}
|
||||
chat:
|
||||
options:
|
||||
model: ${AI_OPENAI_MODEL:}
|
||||
model: ${AI_OPENAI_MODEL:gpt-4o}
|
||||
|
||||
# 不启动 Web 服务器(纯 CLI 模式)
|
||||
main:
|
||||
@@ -27,6 +31,11 @@ spring:
|
||||
virtual:
|
||||
enabled: true
|
||||
|
||||
# === Claude Code Java 自定义配置 ===
|
||||
claude-code:
|
||||
# API 提供者:openai 或 anthropic
|
||||
provider: ${CLAUDE_CODE_PROVIDER:openai}
|
||||
|
||||
# 日志:CLI 模式下只显示 WARN 及以上,保持控制台干净
|
||||
logging:
|
||||
level:
|
||||
|
||||
Reference in New Issue
Block a user