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>
pull/1/head
liuzh 1 month ago
parent 7a6c2fcc02
commit 2adbfa56bc
  1. 11
      run.bat
  2. 11
      run.ps1
  3. 69
      src/main/java/com/claudecode/config/AppConfig.java
  4. 19
      src/main/java/com/claudecode/repl/ReplSession.java
  5. 19
      src/main/resources/application.yml

@ -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
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 @Bean
public TokenTracker tokenTracker() { 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:

Loading…
Cancel
Save