15 KiB
OpenAI 协议兼容层
概述
claude-code 支持通过 OpenAI Chat Completions API(/v1/chat/completions)兼容任意 OpenAI 协议端点,包括 Ollama、DeepSeek、vLLM、One API、LiteLLM 等。
核心策略为流适配器模式:在 queryModel() 中插入提前返回分支,将 Anthropic 格式请求转为 OpenAI 格式,调用 OpenAI SDK,再将 SSE 流转换回 BetaRawMessageStreamEvent 格式。下游代码(流处理循环、query.ts、QueryEngine.ts、REPL)完全不改。
环境变量
| 变量 | 必需 | 说明 |
|---|---|---|
CLAUDE_CODE_USE_OPENAI |
是 | 设为 1 启用 OpenAI 后端 |
OPENAI_API_KEY |
是 | API key(Ollama 等可设为任意值) |
OPENAI_BASE_URL |
推荐 | 端点 URL(如 http://localhost:11434/v1) |
OPENAI_MODEL |
可选 | 覆盖所有请求的模型名(跳过映射) |
OPENAI_DEFAULT_OPUS_MODEL |
可选 | 覆盖 opus 家族对应的模型(如 o3, o3-mini, o1-pro) |
OPENAI_DEFAULT_SONNET_MODEL |
可选 | 覆盖 sonnet 家族对应的模型(如 gpt-4o, gpt-4.1) |
OPENAI_DEFAULT_HAIKU_MODEL |
可选 | 覆盖 haiku 家族对应的模型(如 gpt-4o-mini, gpt-4.0-mini) |
OPENAI_ORG_ID |
可选 | Organization ID |
OPENAI_PROJECT_ID |
可选 | Project ID |
使用示例
# Ollama
CLAUDE_CODE_USE_OPENAI=1 \
OPENAI_API_KEY=ollama \
OPENAI_BASE_URL=http://localhost:11434/v1 \
OPENAI_MODEL=qwen2.5-coder-32b \
bun run dev
# DeepSeek(自动支持 Thinking)
CLAUDE_CODE_USE_OPENAI=1 \
OPENAI_API_KEY=sk-xxx \
OPENAI_BASE_URL=https://api.deepseek.com/v1 \
OPENAI_MODEL=deepseek-chat \
bun run dev
# vLLM
CLAUDE_CODE_USE_OPENAI=1 \
OPENAI_API_KEY=token-abc123 \
OPENAI_BASE_URL=http://localhost:8000/v1 \
OPENAI_MODEL=Qwen/Qwen2.5-Coder-32B-Instruct \
bun run dev
# One API / LiteLLM
CLAUDE_CODE_USE_OPENAI=1 \
OPENAI_API_KEY=sk-your-key \
OPENAI_BASE_URL=https://your-one-api.example.com/v1 \
OPENAI_MODEL=gpt-4o \
bun run dev
# 自定义模型映射(使用家族变量)
CLAUDE_CODE_USE_OPENAI=1 \
OPENAI_API_KEY=sk-xxx \
OPENAI_BASE_URL=https://my-gateway.example.com/v1 \
OPENAI_DEFAULT_SONNET_MODEL="gpt-4o-2024-11-20" \
OPENAI_DEFAULT_HAIKU_MODEL="gpt-4o-mini" \
bun run dev
架构
请求流程
queryModel() [claude.ts]
├── 共享预处理(消息归一化、工具过滤、媒体裁剪)
└── if (getAPIProvider() === 'openai')
└── queryModelOpenAI() [openai/index.ts]
├── resolveOpenAIModel() → 解析模型名
├── normalizeMessagesForAPI() → 共享消息预处理
├── toolToAPISchema() → 构建工具 schema
├── anthropicMessagesToOpenAI() → 消息格式转换
├── anthropicToolsToOpenAI() → 工具格式转换
├── openai.chat.completions.create({ stream: true })
└── adaptOpenAIStreamToAnthropic() → 流格式转换
├── delta.reasoning_content → thinking 块
├── delta.content → text 块
├── delta.tool_calls → tool_use 块
├── usage.cached_tokens → cache_read_input_tokens
└── yield BetaRawMessageStreamEvent
模型名解析优先级
resolveOpenAIModel() 的解析顺序:
OPENAI_MODEL环境变量 → 直接使用,覆盖所有OPENAI_DEFAULT_{FAMILY}_MODEL变量(如OPENAI_DEFAULT_SONNET_MODEL)→ 按模型家族覆盖ANTHROPIC_DEFAULT_{FAMILY}_MODEL变量(向后兼容)- 内置默认映射(见下表)
- 以上都不匹配 → 原名透传
内置模型映射
| Anthropic 模型 | OpenAI 映射 |
|---|---|
claude-sonnet-4-6 |
gpt-4o |
claude-sonnet-4-5-20250929 |
gpt-4o |
claude-sonnet-4-20250514 |
gpt-4o |
claude-3-7-sonnet-20250219 |
gpt-4o |
claude-3-5-sonnet-20241022 |
gpt-4o |
claude-opus-4-6 |
o3 |
claude-opus-4-5-20251101 |
o3 |
claude-opus-4-1-20250805 |
o3 |
claude-opus-4-20250514 |
o3 |
claude-haiku-4-5-20251001 |
gpt-4o-mini |
claude-3-5-haiku-20241022 |
gpt-4o-mini |
同时会自动剥离 [1m] 后缀(Claude 特有的 modifier)。
文件结构
新增文件
src/services/api/openai/
├── client.ts # OpenAI SDK 客户端工厂(~50 行)
├── convertMessages.ts # Anthropic → OpenAI 消息格式转换(~190 行)
├── convertTools.ts # Anthropic → OpenAI 工具格式转换(~70 行)
├── streamAdapter.ts # SSE 流转换核心,含 thinking + caching(~270 行)
├── modelMapping.ts # 模型名解析(~60 行)
├── index.ts # 公共入口 queryModelOpenAI()(~110 行)
└── __tests__/
├── convertMessages.test.ts # 10 个测试
├── convertTools.test.ts # 7 个测试
├── modelMapping.test.ts # 6 个测试
└── streamAdapter.test.ts # 14 个测试(含 thinking + caching)
修改文件
| 文件 | 改动 |
|---|---|
src/utils/model/providers.ts |
添加 'openai' provider 类型 + CLAUDE_CODE_USE_OPENAI 检查(最高优先级) |
src/utils/model/configs.ts |
每个 ModelConfig 添加 openai 键 |
src/services/api/claude.ts |
在 stripExcessMediaItems() 后插入 OpenAI 提前返回分支(~8 行) |
package.json |
添加 "openai": "^4.73.0" 依赖 |
消息转换规则
Anthropic → OpenAI
| Anthropic | OpenAI |
|---|---|
system prompt(string[]) |
role: "system" 消息(\n\n 拼接) |
user + text 块 |
role: "user" 消息 |
assistant + text 块 |
role: "assistant" + content |
assistant + tool_use 块 |
role: "assistant" + tool_calls[] |
user + tool_result 块 |
role: "tool" + tool_call_id |
thinking 块 |
静默丢弃(请求侧) |
工具转换
| Anthropic | OpenAI |
|---|---|
{ name, description, input_schema } |
{ type: "function", function: { name, description, parameters } } |
cache_control, defer_loading 等字段 |
剥离 |
tool_choice: { type: "auto" } |
"auto" |
tool_choice: { type: "any" } |
"required" |
tool_choice: { type: "tool", name } |
{ type: "function", function: { name } } |
消息转换示例
Anthropic: OpenAI:
[
system: ["You are helpful."], [
{ role: "system",
{ role: "user", content: "You are helpful." },
content: [ { role: "user",
{ type: "text", text: "Run ls" } content: "Run ls"
] },
}, { role: "assistant",
{ role: "assistant", content: "I'll check.",
content: [ tool_calls: [{
{ type: "text", text: "I'll check."}, id: "tu_123",
{ type: "tool_use", type: "function",
id: "tu_123", name: "bash", function: {
input: { command: "ls" } } name: "bash",
] arguments: '{"command":"ls"}'
}, }] }
{ role: "user", { role: "tool",
content: [ tool_call_id: "tu_123",
{ type: "tool_result", content: "file1\nfile2"
tool_use_id: "tu_123", }
content: "file1\nfile2" ]
]
}
]
流转换规则
SSE Chunk → Anthropic Event 映射
| OpenAI Chunk | Anthropic Event |
|---|---|
| 首个 chunk | message_start(含 usage) |
delta.reasoning_content |
content_block_start(thinking) + thinking_delta |
delta.content |
content_block_start(text) + text_delta |
delta.tool_calls |
content_block_start(tool_use) + input_json_delta |
finish_reason: "stop" |
message_delta(stop_reason: "end_turn") |
finish_reason: "tool_calls" |
message_delta(stop_reason: "tool_use") |
finish_reason: "length" |
message_delta(stop_reason: "max_tokens") |
块顺序
当模型返回 reasoning_content 时(如 DeepSeek),块顺序与 Anthropic 一致:
thinking block (index 0) ← delta.reasoning_content
text block (index 1) ← delta.content
或:
thinking block (index 0) ← delta.reasoning_content
tool_use block (index 1) ← delta.tool_calls
无 reasoning_content 时:
text block (index 0) ← delta.content
tool_use block (index 1) ← delta.tool_calls(如果有)
finish_reason 映射
| OpenAI | Anthropic |
|---|---|
stop |
end_turn |
tool_calls |
tool_use |
length |
max_tokens |
content_filter |
end_turn |
事件序列示例
纯文本响应:
OpenAI chunks:
delta.content = "Hello"
delta.content = " world"
finish_reason = "stop"
→ Anthropic events:
message_start { message: { id, role: 'assistant', usage: {...} } }
content_block_start { index: 0, content_block: { type: 'text' } }
content_block_delta { index: 0, delta: { type: 'text_delta', text: 'Hello' } }
content_block_delta { index: 0, delta: { type: 'text_delta', text: ' world' } }
content_block_stop { index: 0 }
message_delta { delta: { stop_reason: 'end_turn' } }
message_stop
Thinking + 文本(DeepSeek 风格):
OpenAI chunks:
delta.reasoning_content = "Let me think..."
delta.reasoning_content = " step by step."
delta.content = "The answer is 42."
finish_reason = "stop"
→ Anthropic events:
message_start { ... }
content_block_start { index: 0, content_block: { type: 'thinking', signature: '' } }
content_block_delta { index: 0, delta: { type: 'thinking_delta', thinking: 'Let me think...' } }
content_block_delta { index: 0, delta: { type: 'thinking_delta', thinking: ' step by step.' } }
content_block_stop { index: 0 }
content_block_start { index: 1, content_block: { type: 'text' } }
content_block_delta { index: 1, delta: { type: 'text_delta', text: 'The answer is 42.' } }
content_block_stop { index: 1 }
message_delta { delta: { stop_reason: 'end_turn' } }
message_stop
工具调用:
OpenAI chunks:
delta.tool_calls[0] = { id: 'call_xxx', function: { name: 'bash', arguments: '' } }
delta.tool_calls[0].function.arguments = '{"comm'
delta.tool_calls[0].function.arguments = 'and":"ls"}'
finish_reason = "tool_calls"
→ Anthropic events:
message_start { ... }
content_block_start { index: 0, content_block: { type: 'tool_use', id: 'call_xxx', name: 'bash' } }
content_block_delta { index: 0, delta: { type: 'input_json_delta', partial_json: '{"comm' } }
content_block_delta { index: 0, delta: { type: 'input_json_delta', partial_json: 'and":"ls"}' } }
content_block_stop { index: 0 }
message_delta { delta: { stop_reason: 'tool_use' } }
message_stop
功能支持
Thinking(思维链)
请求侧:不需要显式配置。支持思维链的模型(DeepSeek 等)会自动返回 delta.reasoning_content。
响应侧:delta.reasoning_content 被转换为 Anthropic thinking content block:
// content_block_start
{ type: 'content_block_start', index: 0,
content_block: { type: 'thinking', thinking: '', signature: '' } }
// content_block_delta
{ type: 'content_block_delta', index: 0,
delta: { type: 'thinking_delta', thinking: 'Let me analyze...' } }
thinking block 在 text/tool_use block 之前自动关闭,保持 Anthropic 的块顺序。
Prompt Caching
请求侧:OpenAI 端点使用自动缓存,无需显式设置 cache_control。
响应侧:OpenAI 的 usage.prompt_tokens_details.cached_tokens 被映射到 Anthropic 的 cache_read_input_tokens:
OpenAI: usage.prompt_tokens_details.cached_tokens = 800
↓
Anthropic: message_start.message.usage.cache_read_input_tokens = 800
在 message_start 的 usage 中报告缓存命中量。
工具调用(Tool Use)
完整支持 OpenAI function calling 格式。所有本地工具(Bash、FileEdit、Grep、Glob、Agent 等)透明工作——它们通过 JSON 输入输出通信,格式无关。
工具参数以 input_json_delta 形式流式传输,由下游代码拼接解析。
不支持的功能
| 功能 | 策略 |
|---|---|
| Beta Headers | 不发送 |
| Server Tools (advisor) | 不发送 |
| Structured Output | 不发送 |
| Fast Mode / Effort | 不发送 |
| Tool Search / defer_loading | 不启用,所有工具直接发送 |
| Anthropic Signature | thinking block 的 signature 字段为空字符串 |
| cache_creation_input_tokens | 始终为 0(OpenAI 不区分创建/读取) |
测试
# 运行所有 OpenAI 适配层测试
bun test src/services/api/openai/__tests__/
# 单独运行
bun test src/services/api/openai/__tests__/streamAdapter.test.ts # 14 tests(含 thinking + caching)
bun test src/services/api/openai/__tests__/convertMessages.test.ts # 10 tests
bun test src/services/api/openai/__tests__/convertTools.test.ts # 7 tests
bun test src/services/api/openai/__tests__/modelMapping.test.ts # 6 tests
当前测试覆盖:39 tests / 73 assertions / 0 fail。
测试覆盖矩阵
| 功能 | convertMessages | convertTools | streamAdapter | modelMapping |
|---|---|---|---|---|
| 文本消息转换 | ✅ | |||
| tool_use 转换 | ✅ | |||
| tool_result 转换 | ✅ | |||
| thinking 剥离 | ✅ | |||
| 完整对话流程 | ✅ | |||
| 工具 schema 转换 | ✅ | |||
| tool_choice 映射 | ✅ | |||
| 纯文本流 | ✅ | |||
| 工具调用流 | ✅ | |||
| 混合文本+工具 | ✅ | |||
| finish_reason 映射 | ✅ | |||
| thinking 流 | ✅ | |||
| thinking+text 切换 | ✅ | |||
| thinking+tool_use 切换 | ✅ | |||
| 块索引正确性 | ✅ | |||
| cached_tokens 映射 | ✅ | |||
| OPENAI_MODEL 覆盖 | ✅ | |||
| 默认模型映射 | ✅ | |||
| 未知模型透传 | ✅ | |||
| [1m] 后缀剥离 | ✅ |
端到端验证
# 1. 安装依赖
bun install
# 2. 运行单元测试
bun test src/services/api/openai/__tests__/
# 3. 连接实际端点(以 Ollama 为例)
CLAUDE_CODE_USE_OPENAI=1 \
OPENAI_API_KEY=ollama \
OPENAI_BASE_URL=http://localhost:11434/v1 \
OPENAI_MODEL=qwen2.5-coder-32b \
bun run dev
# 4. 连接 DeepSeek(测试 thinking 支持)
CLAUDE_CODE_USE_OPENAI=1 \
OPENAI_API_KEY=sk-xxx \
OPENAI_BASE_URL=https://api.deepseek.com/v1 \
OPENAI_MODEL=deepseek-reasoner \
bun run dev
# 5. 确认现有测试不受影响
bun test # 无 CLAUDE_CODE_USE_OPENAI 时走原有路径
代码统计
| 类别 | 行数 |
|---|---|
| 新增源码 | ~620 行 |
| 新增测试 | ~450 行 |
| 改动现有代码 | ~25 行 |
| 总计 | ~1100 行 |