-
-
Notifications
You must be signed in to change notification settings - Fork 2.4k
feat(translator): align OpenAI/Gemini/Claude protocol conversion and streaming behavior #1835
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Open
Hsiangpo
wants to merge
1
commit into
router-for-me:main
Choose a base branch
from
Hsiangpo:feat/protocol-align-20260305
base: main
Could not load branches
Branch not found: {{ refName }}
Loading
Could not load tags
Nothing to show
Loading
Are you sure you want to change the base?
Some commits from the old base branch may be removed from the timeline,
and old review comments may become outdated.
Open
Changes from all commits
Commits
File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,146 @@ | ||
| # 2026-03-05 OpenAI 兼容层协议对齐修复记录 | ||
|
|
||
| ## 背景 | ||
| - 目标仓库:`CLIProxyAPI-main.__latest_20260304000229` | ||
| - 修复范围:`OpenAI <-> Claude/Gemini/Gemini-CLI` 的请求与响应转换 | ||
| - 重点:参数兼容、字段类型一致性、流式/非流式行为一致性、工具调用字段保真 | ||
|
|
||
| ## 已修复问题 | ||
|
|
||
| ### 1) `tool_choice` 对象被错误字符串化 | ||
| - 文件:`internal/translator/openai/openai/responses/openai_openai-responses_request.go` | ||
| - 修复:对象/数组使用 `SetRaw`,基础类型使用 `Set` | ||
| - 结果:保留 `tool_choice` 原始 JSON 结构 | ||
|
|
||
| ### 2) Gemini Responses 丢失 `function_call_output.output` 对象 | ||
| - 文件:`internal/translator/gemini/openai/responses/gemini_openai-responses_request.go` | ||
| - 修复:读取 `output` 的 `.Raw/.Value()`,对象走 `SetRaw` | ||
| - 结果:工具输出对象不再丢字段 | ||
|
|
||
| ### 3) Gemini Responses 未映射 OpenAI `stop` | ||
| - 文件:`internal/translator/gemini/openai/responses/gemini_openai-responses_request.go` | ||
| - 修复:同时支持 `stop` 与 `stop_sequences` | ||
| - 结果:统一映射到 `generationConfig.stopSequences` | ||
|
|
||
| ### 4) Gemini Chat 未映射 `max_tokens` / `max_completion_tokens` / `stop` | ||
| - 文件:`internal/translator/gemini/openai/chat-completions/gemini_openai_request.go` | ||
| - 修复: | ||
| - `max_tokens|max_completion_tokens -> generationConfig.maxOutputTokens` | ||
| - `stop|stop_sequences -> generationConfig.stopSequences` | ||
|
|
||
| ### 5) Gemini Chat 图片与文件转换丢参/错形 | ||
| - 文件:`internal/translator/gemini/openai/chat-completions/gemini_openai_request.go` | ||
| - 修复: | ||
| - 新增 `parseDataURI`,拆分 `mime` 与纯 base64 数据 | ||
| - 远程图片 URL 映射到 `fileData.fileUri` | ||
| - 新增 `guessMimeTypeFromURL` 补 `fileData.mimeType` | ||
|
|
||
| ### 6) Gemini Chat/Responses 缺失 `tool_choice` 映射 | ||
| - 文件: | ||
| - `internal/translator/gemini/openai/chat-completions/gemini_openai_request.go` | ||
| - `internal/translator/gemini/openai/responses/gemini_openai-responses_request.go` | ||
| - 修复: | ||
| - `auto -> AUTO` | ||
| - `none -> NONE` | ||
| - `required -> ANY` | ||
| - 指定函数名映射到 `allowedFunctionNames` | ||
|
|
||
| ### 7) Gemini -> OpenAI `finish_reason` 映射不准 + 多候选读取错误 | ||
| - 文件:`internal/translator/gemini/openai/chat-completions/gemini_openai_response.go` | ||
| - 修复: | ||
| - 新增 `mapGeminiFinishReason` | ||
| - `MAX_TOKENS -> length`,并保留 `native_finish_reason=max_tokens` | ||
| - 流式场景按当前 candidate 读取 `finishReason`,不再固定读取 `candidates.0` | ||
|
|
||
| ### 8) Claude Responses 参数透传补齐 | ||
| - 文件:`internal/translator/claude/openai/responses/claude_openai-responses_request.go` | ||
| - 修复: | ||
| - 增加 `input` 字符串到用户消息映射 | ||
| - 增加 `temperature/top_p/stop` 映射 | ||
|
|
||
| ### 9) Claude Chat 远程图片 URL 丢失 | ||
| - 文件:`internal/translator/claude/openai/chat-completions/claude_openai_request.go` | ||
| - 修复:非 data URL 的 `image_url` 映射为 Claude `source.type=url` | ||
|
|
||
| ### 10) `HasResponseTransformer` 方向判断兼容增强 | ||
| - 文件:`sdk/translator/registry.go` | ||
| - 修复:`from->to` 与 `to->from` 双向判定 | ||
| - 结果:避免“可翻译却被判定不存在”问题 | ||
|
|
||
| ### 11) Gemini / Gemini-CLI 工具响应对象保真 | ||
| - 文件: | ||
| - `internal/translator/gemini/openai/chat-completions/gemini_openai_request.go` | ||
| - `internal/translator/gemini-cli/openai/chat-completions/gemini-cli_openai_request.go` | ||
| - 修复:`functionResponse.response.result` 对象走 `SetRaw`,基础类型走 `Set` | ||
| - 结果:工具结果对象不再退化成 JSON 字符串 | ||
|
|
||
| ### 12) Responses -> Chat 不再丢弃 built-in tools | ||
| - 文件:`internal/translator/openai/openai/responses/openai_openai-responses_request.go` | ||
| - 修复:保留 `web_search` / `file_search` 等内建工具定义,不再静默忽略 | ||
| - 结果:跨端点转换时工具能力信息保真 | ||
|
|
||
| ### 13) Gemini-CLI Chat 请求参数对齐补齐 | ||
| - 文件:`internal/translator/gemini-cli/openai/chat-completions/gemini-cli_openai_request.go` | ||
| - 修复: | ||
| - `max_completion_tokens|max_tokens -> request.generationConfig.maxOutputTokens` | ||
| - `stop|stop_sequences -> request.generationConfig.stopSequences` | ||
| - `tool_choice -> request.toolConfig.functionCallingConfig` | ||
| - `image_url` 同时支持 `data:` 与远程 URL(远程映射到 `fileData.fileUri`) | ||
| - 工具结果为 JSON 字符串时尝试反序列化后保留对象结构 | ||
|
|
||
| ### 14) Gemini-CLI -> OpenAI Chat 响应多候选与 finish_reason 对齐 | ||
| - 文件:`internal/translator/gemini-cli/openai/chat-completions/gemini-cli_openai_response.go` | ||
| - 修复: | ||
| - 不再固定读取 `candidates.0`,按 `response.candidates` 遍历输出 chunk | ||
| - `MAX_TOKENS -> finish_reason=length`,并保留 `native_finish_reason=max_tokens` | ||
| - 对 `stop/safety` 等原因进行 OpenAI 语义映射 | ||
|
|
||
| ### 15) OpenAI -> Gemini 响应多候选不再覆盖到 `candidates.0` | ||
| - 文件:`internal/translator/openai/gemini/openai_gemini_response.go` | ||
| - 修复: | ||
| - 流式与非流式均按 choice index 写入 `candidates.{index}` | ||
| - 消除 `n>1` 场景下“后写覆盖前写”的问题 | ||
|
|
||
| ### 16) Gemini -> OpenAI Request 细节补齐 | ||
| - 文件:`internal/translator/openai/gemini/openai_gemini_request.go` | ||
| - 修复: | ||
| - `generationConfig.stop` 字符串输入兼容 | ||
| - `functionCallingConfig.mode=ANY + allowedFunctionNames=[name]` 映射为 OpenAI 指定函数 `tool_choice` 对象 | ||
| - `functionResponse` 的 `tool_call_id` 优先按 `name` 精确匹配,减少错绑 | ||
|
|
||
| ### 17) Claude -> OpenAI Request 停止词别名兼容 | ||
| - 文件:`internal/translator/openai/claude/openai_claude_request.go` | ||
| - 修复:`stop_sequences` 之外,新增 `stop` 字段兼容(字符串/数组) | ||
|
|
||
| ### 18) Gemini -> OpenAI Responses(非流式)多候选输出补齐 | ||
| - 文件:`internal/translator/gemini/openai/responses/gemini_openai-responses_response.go` | ||
| - 修复: | ||
| - 非流式聚合不再固定 `candidates.0` | ||
| - 按所有 `candidates[*].content.parts` 生成 `output` 项,避免多候选文本被静默丢弃 | ||
| - 保持候选内 `function_call/reasoning/message` 的输出语义 | ||
|
|
||
| ### 19) Gemini -> OpenAI Responses(流式)多候选状态机对齐 | ||
| - 文件:`internal/translator/gemini/openai/responses/gemini_openai-responses_response.go` | ||
| - 修复: | ||
| - 流式路径按 `candidates[*]` 逐个处理,不再固定 `candidates.0` | ||
| - 新增按 candidate 维度的 message/reasoning 状态,避免 `n>1` 时互相覆盖 | ||
| - `response.completed` 聚合阶段按 `output_index` 汇总所有 candidate 的 message/reasoning/function_call | ||
| - 补充流式多候选回归测试,覆盖 `response.output` 双候选文本输出 | ||
|
|
||
| ## 新增测试 | ||
| - `internal/translator/openai/openai/responses/openai_openai-responses_request_test.go` | ||
| - `internal/translator/gemini/openai/responses/gemini_openai-responses_request_test.go` | ||
| - `internal/translator/gemini/openai/chat-completions/gemini_openai_request_test.go` | ||
| - `internal/translator/gemini/openai/chat-completions/gemini_openai_response_test.go` | ||
| - `internal/translator/gemini/openai/responses/gemini_openai-responses_response_test.go` | ||
| - `internal/translator/gemini-cli/openai/chat-completions/gemini-cli_openai_request_test.go` | ||
| - `internal/translator/gemini-cli/openai/chat-completions/gemini-cli_openai_response_test.go` | ||
| - `internal/translator/claude/openai/responses/claude_openai-responses_request_test.go` | ||
| - `internal/translator/claude/openai/chat-completions/claude_openai_request_test.go` | ||
| - `internal/translator/openai/gemini/openai_gemini_request_test.go` | ||
| - `internal/translator/openai/gemini/openai_gemini_response_test.go` | ||
|
|
||
| ## 验证命令与结果 | ||
| - `go test ./internal/translator/...`:通过 | ||
| - `go test ./sdk/api/handlers/openai/...`:通过 | ||
| - `go test ./internal/runtime/executor/... -run "OpenAI|compat|Translator"`:通过 | ||
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
32 changes: 32 additions & 0 deletions
32
internal/translator/claude/openai/chat-completions/claude_openai_request_test.go
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,32 @@ | ||
| package chat_completions | ||
|
|
||
| import ( | ||
| "testing" | ||
|
|
||
| "github.com/tidwall/gjson" | ||
| ) | ||
|
|
||
| func TestConvertOpenAIRequestToClaude_KeepRemoteImageURL(t *testing.T) { | ||
| in := []byte(`{ | ||
| "model":"claude-sonnet-4-5", | ||
| "messages":[ | ||
| { | ||
| "role":"user", | ||
| "content":[{"type":"image_url","image_url":{"url":"https://example.com/a.png"}}] | ||
| } | ||
| ] | ||
| }`) | ||
|
|
||
| out := ConvertOpenAIRequestToClaude("claude-sonnet-4-5", in, false) | ||
| root := gjson.ParseBytes(out) | ||
|
|
||
| if got := root.Get("messages.0.content.0.type").String(); got != "image" { | ||
| t.Fatalf("image type mismatch: got=%q output=%s", got, string(out)) | ||
| } | ||
| if got := root.Get("messages.0.content.0.source.type").String(); got != "url" { | ||
| t.Fatalf("image source type mismatch: got=%q output=%s", got, string(out)) | ||
| } | ||
| if got := root.Get("messages.0.content.0.source.url").String(); got != "https://example.com/a.png" { | ||
| t.Fatalf("image url mismatch: got=%q output=%s", got, string(out)) | ||
| } | ||
| } |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
37 changes: 37 additions & 0 deletions
37
internal/translator/claude/openai/responses/claude_openai-responses_request_test.go
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,37 @@ | ||
| package responses | ||
|
|
||
| import ( | ||
| "testing" | ||
|
|
||
| "github.com/tidwall/gjson" | ||
| ) | ||
|
|
||
| func TestConvertOpenAIResponsesRequestToClaude_MapInputStringAndParams(t *testing.T) { | ||
| in := []byte(`{ | ||
| "model":"claude-sonnet-4-5", | ||
| "input":"hello", | ||
| "max_output_tokens":256, | ||
| "temperature":0.2, | ||
| "stop":["END"] | ||
| }`) | ||
|
|
||
| out := ConvertOpenAIResponsesRequestToClaude("claude-sonnet-4-5", in, false) | ||
| root := gjson.ParseBytes(out) | ||
|
|
||
| if got := root.Get("messages.0.role").String(); got != "user" { | ||
| t.Fatalf("input string should map to user message, got role=%q output=%s", got, string(out)) | ||
| } | ||
| if got := root.Get("messages.0.content").String(); got != "hello" { | ||
| t.Fatalf("input string should map to user message content, got=%q output=%s", got, string(out)) | ||
| } | ||
| if got := root.Get("max_tokens").Int(); got != 256 { | ||
| t.Fatalf("max_output_tokens mapping mismatch: got=%d output=%s", got, string(out)) | ||
| } | ||
| if got := root.Get("temperature").Float(); got != 0.2 { | ||
| t.Fatalf("temperature mapping mismatch: got=%v output=%s", got, string(out)) | ||
| } | ||
| stop := root.Get("stop_sequences") | ||
| if !stop.Exists() || !stop.IsArray() || len(stop.Array()) != 1 || stop.Array()[0].String() != "END" { | ||
| t.Fatalf("stop mapping mismatch: %s output=%s", stop.Raw, string(out)) | ||
| } | ||
| } |
Oops, something went wrong.
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
文件名和此文档标题中的日期
2026-03-05似乎是一个拼写错误,很可能意在表示过去的日期(例如2024-03-05)。使用未来日期可能会引起混淆。最好将其更正以反映更改的实际日期。