Skip to main content

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_KEYAPI key(Ollama 等可设为任意值)
OPENAI_BASE_URL推荐端点 URL(如 http://localhost:11434/v1
OPENAI_MODEL可选覆盖所有请求的模型名(跳过映射)
OPENAI_MODEL_MAP可选JSON 映射,如 {"claude-sonnet-4-6":"gpt-4o"}
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_MODEL_MAP='{"claude-sonnet-4-6":"gpt-4o-2024-11-20","claude-haiku-4-5":"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() 的解析顺序:
  1. OPENAI_MODEL 环境变量 → 直接使用,覆盖所有
  2. OPENAI_MODEL_MAP JSON 查表 → 自定义映射
  3. 内置默认映射(见下表)
  4. 以上都不匹配 → 原名透传

内置模型映射

Anthropic 模型OpenAI 映射
claude-sonnet-4-6gpt-4o
claude-sonnet-4-5-20250929gpt-4o
claude-sonnet-4-20250514gpt-4o
claude-3-7-sonnet-20250219gpt-4o
claude-3-5-sonnet-20241022gpt-4o
claude-opus-4-6o3
claude-opus-4-5-20251101o3
claude-opus-4-1-20250805o3
claude-opus-4-20250514o3
claude-haiku-4-5-20251001gpt-4o-mini
claude-3-5-haiku-20241022gpt-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.tsstripExcessMediaItems() 后插入 OpenAI 提前返回分支(~8 行)
package.json添加 "openai": "^4.73.0" 依赖

消息转换规则

Anthropic → OpenAI

AnthropicOpenAI
system prompt(string[]role: "system" 消息(\n\n 拼接)
user + textrole: "user" 消息
assistant + textrole: "assistant" + content
assistant + tool_userole: "assistant" + tool_calls[]
user + tool_resultrole: "tool" + tool_call_id
thinking静默丢弃(请求侧)

工具转换

AnthropicOpenAI
{ 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 ChunkAnthropic Event
首个 chunkmessage_start(含 usage)
delta.reasoning_contentcontent_block_start(thinking) + thinking_delta
delta.contentcontent_block_start(text) + text_delta
delta.tool_callscontent_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 映射

OpenAIAnthropic
stopend_turn
tool_callstool_use
lengthmax_tokens
content_filterend_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 Signaturethinking 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

测试覆盖矩阵

功能convertMessagesconvertToolsstreamAdaptermodelMapping
文本消息转换
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 行