Skip to content

Add Codex UA detection and instructions handling#178

Merged
Bowl42 merged 8 commits intomainfrom
feature/codex-ua-instructions
Feb 1, 2026
Merged

Add Codex UA detection and instructions handling#178
Bowl42 merged 8 commits intomainfrom
feature/codex-ua-instructions

Conversation

@Bowl42
Copy link
Collaborator

@Bowl42 Bowl42 commented Feb 1, 2026

Summary

  • Add Codex/OpenCode user-agent detection and inject system instructions for compatible clients
  • Add /v1/models endpoint with Claude thinking budget guard
  • Refine client detection logic and count_tokens handling
  • Refactor large converter files into smaller, focused modules (request/response/stream/helpers)

Test Plan

  • All existing tests pass
  • New tests added for models handler, client detection, codex instructions, and converter modules
  • Manual verification with Codex CLI client

🤖 Generated with Claude Code

Summary by CodeRabbit

发行说明

  • 新功能

    • 添加了对 Claude CLI 用户代理的自动检测功能
    • 新增 /v1/responses 端点模式支持
    • 增强了推理能力和思维预算配置管理
    • 加入对 Codex 代理的全面支持
  • 改进

    • 改进了工具参数映射和兼容性处理
    • 优化了流式响应转换性能
    • 增强了跨多个 AI 模型的请求/响应转换可靠性
    • 提升了会话 ID 提取和处理能力
  • 测试

    • 添加了全面的转换路径覆盖测试
    • 增加了流式转换场景测试

✏️ Tip: You can customize this high-level summary in your review settings.

@coderabbitai
Copy link

coderabbitai bot commented Feb 1, 2026

Caution

Review failed

The pull request is closed.

📝 Walkthrough

总体概览

Walkthrough

此PR重构了多个系统组件,包括客户端适配器增强、请求处理程序配置更新、转换器框架重组(移除旧实现,新增多个转换器)、新增辅助模块以及广泛的测试覆盖。

Changes

群组 / 文件 摘要
配置和主程序
.gitignore, cmd/maxx/main.go
新增 .tmp/ 忽略模式;新增 ModelsHandler 创建并将其接入 ProjectProxyHandler 构造函数。
客户端适配器
internal/adapter/client/adapter.go, internal/adapter/client/adapter_test.go
新增 Claude 用户代理检测;新增 /v1/responses 端点映射;新增 ExtractSessionID 公共方法;重构基于字节的检测路径。
Claude 请求处理
internal/adapter/provider/custom/claude_body.go, internal/adapter/provider/custom/claude_body_test.go
新增 ensureMinThinkingBudget 辅助函数来强制最小思考令牌预算;新增对应单元测试。
删除的转换器
internal/converter/claude_to_gemini.go, internal/converter/claude_to_openai.go, internal/converter/openai_to_gemini.go, internal/converter/gemini_to_claude.go
完全删除旧的 Claude-to-Gemini、Claude-to-OpenAI、OpenAI-to-Gemini 和 Gemini-to-Claude 转换器实现。
新增 Claude-to-Gemini 转换器
internal/converter/claude_to_gemini_helpers.go, internal/converter/claude_to_gemini_request.go, internal/converter/claude_to_gemini_response.go, internal/converter/claude_to_gemini_stream.go, internal/converter/claude_to_gemini_*_test.go
新增完整的 Claude-to-Gemini 转换实现,包含请求/响应转换、流处理、助手工具函数和广泛的测试覆盖。
新增 Claude-to-OpenAI 转换器
internal/converter/claude_to_openai_helpers.go, internal/converter/claude_to_openai_request.go, internal/converter/claude_to_openai_response.go, internal/converter/claude_to_openai_stream.go, internal/converter/claude_to_openai_test.go
新增 Claude-to-OpenAI 转换实现,处理思考到推理的映射、工具调用和流式转换。
新增 Codex 相关转换
internal/converter/codex_instructions.go, internal/converter/codex_to_*.go, internal/converter/*_to_codex.go
新增 Codex 指令处理、用户代理管理、Codex 到多个格式的转换以及相反方向的转换。
新增 OpenAI-to-Gemini 转换器
internal/converter/openai_to_gemini_helpers.go, internal/converter/openai_to_gemini_request.go, internal/converter/openai_to_gemini_response.go
新增 OpenAI-to-Gemini 请求和响应转换,包含工具选择、推理配置和内容映射。
新增 Gemini-to-Claude 转换器
internal/converter/gemini_to_claude_helpers.go, internal/converter/gemini_to_claude_request.go, internal/converter/gemini_to_claude_response.go, internal/converter/gemini_to_claude_stream.go
新增 Gemini-to-Claude 转换实现,包含函数调用参数映射和流式处理。
更新的转换器
internal/converter/claude_to_codex.go, internal/converter/codex_to_gemini.go, internal/converter/codex_to_openai.go, internal/converter/gemini_to_openai.go, internal/converter/openai_to_claude_*.go, internal/converter/openai_to_codex.go
多个现有转换器添加推理配置映射、工具名称缩短、指令处理等功能。
转换器测试覆盖
internal/converter/coverage_*.go, internal/converter/*_more_test.go, internal/converter/gemini_openai_test.go, internal/converter/more_converter_test.go, internal/converter/openai_gemini_multimodal_test.go
新增广泛的测试文件,覆盖转换器流、边界情况、错误处理、流式转换和跨格式交互。
Codex 指令文档
internal/converter/codex_instructions/*.md
新增多个 Markdown 指令文档(default.md、codex.md、codex_max.md、gpt51.md、gpt52.md、gpt52_codex.md),定义 Codex CLI 代理的操作模型。

代码审查工作量估算

🎯 4 (复杂) | ⏱️ ~60 分钟

原因: 此PR涉及高度异构的变化,跨越多个不相关的系统领域。包含大规模转换器框架重组(删除999行,新增3000+行)、新的适配器逻辑、多个转换器实现的添加/修改、以及~3000+行的测试代码。需要理解多个转换路径、流处理逻辑、Claude/OpenAI/Gemini/Codex 格式间的映射,以及新指令框架。虽然许多文件是新添加的,但理解转换器之间的互联关系和测试覆盖的有效性需要大量认知投入。

可能相关的PRs

建议的审阅者

  • awsl233777

🐰 一只兔子的诗

在转换的迷宫中蹦蹦跳,
旧的路径留下,新的绽放光耀,
Claude、OpenAI、Gemini 翩翩起舞,
Codex 的指令如魔法般闪烁,
代理们现在理解四种语言,
测试覆盖如胡萝卜堆积成山! 🥕✨

🚥 Pre-merge checks | ✅ 2 | ❌ 1
❌ Failed checks (1 warning)
Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 6.67% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (2 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed 标题"Add Codex UA detection and instructions handling"准确描述了PR的主要变更,包括新增对Codex/OpenCode用户代理的检测和系统指令的注入功能。

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing touches
  • 📝 Generate docstrings
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Post copyable unit tests in a comment
  • Commit unit tests in branch feature/codex-ua-instructions

📜 Recent review details

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between c378448 and c68c042.

📒 Files selected for processing (12)
  • internal/converter/claude_to_codex.go
  • internal/converter/codex_instructions.go
  • internal/converter/codex_instructions/codex.md
  • internal/converter/codex_instructions/codex_max.md
  • internal/converter/codex_instructions/default.md
  • internal/converter/codex_instructions/gpt51.md
  • internal/converter/codex_instructions/gpt52.md
  • internal/converter/codex_instructions/gpt52_codex.md
  • internal/converter/coverage_codex_instructions_test.go
  • internal/converter/coverage_misc_helpers_test.go
  • internal/converter/gemini_to_codex.go
  • internal/converter/openai_to_codex.go

✏️ Tip: You can disable this entire section by setting review_details to false in your review settings.


Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 19

Note

Due to the large number of review comments, Critical, Major severity comments were prioritized as inline comments.

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
internal/adapter/client/adapter.go (1)

228-239: ⚠️ Potential issue | 🟠 Major

DetectClientType 缺少 /v1/responses 端点检测,与 Match 方法不一致。

Match 方法(第 44-45 行)已添加对 /v1/responses 路径的处理,但 DetectClientType 方法的 switch 语句中缺少此 case。这会导致两个方法对同一请求返回不同的客户端类型。

🐛 修复建议:添加缺失的端点检测
 switch {
 case strings.HasPrefix(path, "/v1/messages"):
     return domain.ClientTypeClaude
 case strings.HasPrefix(path, "/responses"):
     return domain.ClientTypeCodex
+case strings.HasPrefix(path, "/v1/responses"):
+    return domain.ClientTypeCodex
 case strings.HasPrefix(path, "/v1/chat/completions"):
     return domain.ClientTypeOpenAI
 case strings.HasPrefix(path, "/v1beta/models/"):
     return domain.ClientTypeGemini
 case strings.HasPrefix(path, "/v1internal/models/"):
     return domain.ClientTypeGemini
 }
🤖 Fix all issues with AI agents
In `@internal/adapter/provider/custom/claude_body.go`:
- Around line 103-117: The function ensureMinThinkingBudget is using the wrong
JSON path "thinking.enabled.budget_tokens"; update both gjson.GetBytes and
sjson.SetBytes calls to use "thinking.budget_tokens" instead so the
check/assignment targets the actual Claude API field, keeping the same minBudget
constant and existing result.Type/Int checks and error handling.

In `@internal/converter/claude_to_gemini_request.go`:
- Around line 241-246: 当前代码在将工具调用参数 (变量 input :=
m["input"].(map[string]interface{})) 传入 cleanJSONSchema,这会改写/删除用户真实实参字段(例如
type、properties、required)。请改为只对工具定义的 schema 清理:移除对 cleanJSONSchema(input)
的调用,改为在工具定义处调用 cleanJSONSchema(工具的 input_schema)(参考变量/字段名
input_schema、tool_use、m、input、cleanJSONSchema),确保不修改 m["input"] 的内容并保留用户原始实参。
- Around line 239-309: When constructing Gemini parts skip building invalid
FunctionCall/FunctionResponse when required identifiers are missing: for the
"tool_call" branch ensure both id and name are non-empty before appending a
GeminiPart with GeminiFunctionCall (use variables id, name, GeminiPart,
GeminiFunctionCall, toolIDToName, parts) — if either is missing, either continue
(skip) or return an error; for the "tool_result" branch require a non-empty
tool_use_id and resolve funcName from toolIDToName (variables tool_use_id,
funcName, GeminiFunctionResponse) and if funcName is empty do not append the
part (or return an error) so you never create requests with empty name/ID; add
brief logging where you skip to aid debugging.

In `@internal/converter/claude_to_gemini_stream.go`:
- Around line 35-38: The code in the "message_delta" branch dereferences
state.Usage (setting state.Usage.OutputTokens) without ensuring it's non-nil;
update the handler in claude_to_gemini_stream.go so you nil-check state.Usage
and initialize it (e.g., state.Usage = &Usage{}) before assigning OutputTokens;
reference the TransformState struct, the state.Usage pointer field, and the
Usage.OutputTokens field when making this change.
- Around line 46-50: The code constructs a GeminiUsageMetadata using
state.Usage.InputTokens and state.Usage.OutputTokens without nil-checking
state.Usage, which will panic in the message_stop event if state.Usage is nil;
update the message_stop handling (where UsageMetadata is set) to first test if
state.Usage != nil and only then populate PromptTokenCount, CandidatesTokenCount
and TotalTokenCount from state.Usage, otherwise set those fields to zero (or
leave them unset) so access to state.Usage.InputTokens/OutputTokens is guarded
and no panic occurs.

In `@internal/converter/claude_to_openai_stream.go`:
- Around line 107-109: The assignment to state.Usage.OutputTokens risks a
nil-pointer panic—before setting OutputTokens when claudeEvent.Usage != nil,
check if state.Usage is nil and if so initialize it (same pattern used in
claude_to_gemini_stream.go), then copy claudeEvent.Usage.OutputTokens into
state.Usage.OutputTokens; ensure you reference the same struct type used for
state.Usage to create the new instance.
- Around line 44-49: 在处理 claudeEvent.ContentBlock.Type == "tool_use" 分支时,直接写入
state.ToolCalls 可能在该 map 为 nil 时导致 panic;在函数 TransformChunk 或在该分支开始前检查
state.ToolCalls 是否为 nil 并在需要时初始化(例如在 claude_to_openai_stream.go 中使用
state.ToolCalls、ToolCallState、TransformChunk 的位置),或者确保 TransformState
在被传入前已初始化,这样再执行 state.ToolCalls[claudeEvent.Index] = &ToolCallState{...}
就不会触发运行时错误。

In
`@internal/converter/codex_instructions/prompt.md-001-31d0d7a305305ad557035a2edcab60b6be5018d8`:
- Around line 12-88: The prompt uses the wrong apply_patch field and invalid
patch headers: replace the JSON key {"cmd": [...]} with {"command": [...]} (so
tools accept the action) and change the nonstandard markers "**_" / "_**" to the
standard "***" patch envelope (e.g., "*** Begin Patch" and "*** End Patch")
throughout the file (look for the example apply_patch usage and all occurrences
of "**_ Begin Patch", "_** End Patch", and any "**_ Update File:" / "_** Move
to:" lines) so the embedded patch examples conform to the apply-patch spec.

In
`@internal/converter/codex_instructions/prompt.md-002-6ce0a5875bbde55a00df054e7f0bceba681cf44d`:
- Around line 12-88: The prompt uses the wrong apply_patch invocation field and
invalid patch header markers: replace the JSON key {"cmd": [...]} with
{"command": [...]} when invoking apply_patch and normalize the patch envelope
markers from the invalid "**_" / "_**" tokens to the correct "*** Begin Patch" /
"*** End Patch" (and "*** Update File:", "*** Add File:", "*** Delete File:"
etc.) format so the apply_patch payload conforms to the specified patch grammar;
update occurrences around the apply_patch example and any header lines using
"**_" or "_**" accordingly.

In
`@internal/converter/codex_instructions/prompt.md-003-a6139aa0035d19d794a3669d6196f9f32a8c8352`:
- Around line 59-62: Update the apply_patch line-prefix description in prompt.md
to use the correct symbols: change the current "for inserted text, * for removed
text" phrasing to "for inserted text, - for removed text" (i.e., use '+' for
inserted and '-' for removed, with space for context) so it matches the
apply_patch syntax and examples; locate the text in
internal/converter/codex_instructions/prompt.md near the apply_patch explanation
and replace the incorrect '-'/'*' wording with '+'/'-' accordingly.

In
`@internal/converter/codex_instructions/prompt.md-006-81b148bda271615b37f7e04b3135e9d552df8111`:
- Around line 261-306: The patch syntax examples use an invalid "**_ " prefix
(e.g., "**_ Begin Patch", "**_ Add File:", "**_ Update File:", "**_ Delete
File:") which should be the recognized "*** " prefix; update the documentation
text in the prompt (the block including "Begin Patch", "Add File:", "Update
File:", "Delete File:", "End Patch" and the example patch at the end) to replace
every "**_ " prefix with the correct "*** " prefix so the apply_patch grammar
and examples (symbols like "**_ Begin Patch", "**_ Update File:", and the
example patch) use the valid "***" header format consistently.

In `@internal/converter/gemini_to_claude_request.go`:
- Around line 74-81: The code ignores json.Marshal errors and sets
ClaudeContentBlock.ToolUseID to part.FunctionResponse.Name which can mismatch
the generated tool call ID; update the block in the conversion logic that
handles part.FunctionResponse to check and handle the error returned by
json.Marshal (e.g., return or log the error) before using respJSON, use
part.FunctionResponse.ID (not Name) when setting ClaudeContentBlock.ToolUseID,
and ensure the ID you choose matches the generator where tool calls are produced
(the fmt.Sprintf("call_%d", toolCallCounter) logic) so the produced tool_result
blocks correctly reference the tool_use blocks created earlier.

In `@internal/converter/gemini_to_codex.go`:
- Around line 228-233: 当前代码强制将 codexReq.Stream 设为 true,覆盖了 Transform 的 stream
入参并把非流式请求错误地转换为流式;请把对 codexReq.Stream 的硬编码移除或改为使用传入的 stream 参数(即保留 Transform 的
stream 值),确保在构造请求时尊重 Transform 的入参;在本文件中查找 codexReq.Stream 赋值并替换为使用 Transform 的
stream 或直接删除该覆盖(同时保留对 codexReq 的其它字段赋值,确保 CodexInstructionsForModel 调用不被影响)。

In `@internal/converter/openai_to_claude_request.go`:
- Around line 100-109: The loop over msg.ToolCalls currently ignores
json.Unmarshal errors, causing ClaudeContentBlock.Input to become nil silently;
modify the loop in the code that builds ClaudeContentBlock (the section that
iterates msg.ToolCalls) to capture the error returned by json.Unmarshal into a
variable, and if non-nil return or propagate that error (wrap it with context
including tc.Function.Name and tc.ID) instead of continuing, so failures parsing
tc.Function.Arguments are reported rather than silenced.
- Around line 34-55: The converter currently treats messages with Role
"developer" as regular messages and sends Role: "developer" into Claude, which
only accepts "user" and "assistant"; update the loop that processes req.Messages
(the switch handling system messages) to treat Role "developer" the same as
"system": extract and append its content into claudeReq.System using the same
string and []interface{} parsing logic; ensure you do not create a ClaudeMessage
for developer (avoid adding a ClaudeMessage{Role: msg.Role} for developer) and
mirror the handling used in openai_to_gemini_request.go so claudeReq.System
contains both system and developer content.

In `@internal/converter/openai_to_claude_response.go`:
- Around line 63-71: In the loop over choice.Message.ToolCalls inside Transform,
json.Unmarshal is currently called without checking its error which can produce
nil/invalid tool_use blocks; before unmarshalling, check if
tc.Function.Arguments is an empty string and return an error if so, then call
json.Unmarshal into input and if it returns an error, return a formatted error
(including tc.ID and tc.Function.Name) instead of silently proceeding; only
append the ClaudeContentBlock to claudeResp.Content when unmarshalling succeeds.

In `@internal/converter/openai_to_gemini_request.go`:
- Around line 179-186: The loop that converts msg.ToolCalls builds
GeminiPart.FunctionCall by unmarshaling tc.Function.Arguments with
json.Unmarshal but ignores the error, which can produce nil Args and a bad
functionCall; update the conversion (the code that appends to
geminiContent.Parts / constructs GeminiPart and GeminiFunctionCall) to check the
json.Unmarshal error for each tc, and if unmarshaling fails return or propagate
a descriptive error (including the function name tc.Function.Name and the raw
arguments string) instead of appending a function call with nil Args so
downstream callers don't receive invalid data.
- Around line 72-84: The code currently maps req.ReasoningEffort directly to
geminiReq.GenerationConfig.ThinkingConfig.ThinkingLevel, which causes an invalid
"none" enum to be sent; update the logic in the block handling
req.ReasoningEffort so that when effort == "none" you set IncludeThoughts =
false and do NOT assign ThinkingLevel (leave it zero-value so omitempty removes
it), but for other values assign ThinkingLevel = effort and set IncludeThoughts
= effort != "none"; retain the existing ThinkingBudget handling for "auto" and
ensure you only create ThinkingConfig when needed (refer to req.ReasoningEffort,
geminiReq.GenerationConfig.ThinkingConfig, ThinkingLevel, IncludeThoughts,
ThinkingBudget).

In `@internal/converter/openai_to_gemini_response.go`:
- Around line 54-62: The code silently ignores json.Unmarshal errors when
parsing tc.Function.Arguments inside the loop over choice.Message.ToolCalls,
which can drop function_call args; update the parsing in the converter (the loop
handling choice.Message.ToolCalls that appends GeminiPart with
GeminiFunctionCall) to check the error returned by json.Unmarshal for
tc.Function.Arguments and propagate/return that error (or wrap it with context
including tc.Function.Name) instead of continuing, so callers can handle the
failure and the candidate.Content.Parts is not populated with an empty Args map.
🟡 Minor comments (26)
internal/converter/gemini_openai_test.go-18-19 (1)

18-19: ⚠️ Potential issue | 🟡 Minor

处理 json.Marshal 的错误,避免隐藏测试失败。

在测试中忽略 Marshal 的错误会导致 Transform 收到空输入,掩盖真实问题。建议显式检查错误。

建议修复
-	body, _ := json.Marshal(req)
+	body, err := json.Marshal(req)
+	if err != nil {
+		t.Fatalf("marshal: %v", err)
+	}

Also applies to: 52-53

internal/converter/codex_openai_more_test.go-8-25 (1)

8-25: ⚠️ Potential issue | 🟡 Minor

补上 json.Marshal 的错误处理以避免误报
Line 13 忽略了 json.Marshal 的错误,可能导致测试在序列化失败时仍继续执行并产生误导性结果。

✅ 建议修改
-	body, _ := json.Marshal(req)
+	body, err := json.Marshal(req)
+	if err != nil {
+		t.Fatalf("marshal: %v", err)
+	}
 	conv := &codexToOpenAIRequest{}
-	out, err := conv.Transform(body, "gpt-test", false)
+	out, err := conv.Transform(body, "gpt-test", false)
internal/converter/codex_instructions/gpt_5_1_prompt.md-003-daf77b845230c35c325500ff73fe72a78f3b7416-48-49 (1)

48-49: ⚠️ Potential issue | 🟡 Minor

澄清“首次工具调用前必须给计划”与“简单任务不该用计划”的冲突

Line 48 的“首次工具调用前给计划”与 Line 67-68 的“简单/单步任务不要用计划”相互矛盾,容易让执行者在简单任务前不知是否必须出计划。建议在 Line 48 增加“仅当需要计划时”的限定,以消除歧义。

✅ 建议改写(示例)
- Before the first tool call, give a quick plan with goal, constraints, next steps.
+ If a plan is warranted, give a quick plan with goal, constraints, next steps before the first tool call.

Also applies to: 67-68

internal/converter/codex_instructions/gpt_5_1_prompt.md-001-ec69a4a810504acb9ba1d1532f98f9db6149d660-38-38 (1)

38-38: ⚠️ Potential issue | 🟡 Minor

语法问题:句子结构不完整

第38行存在语法错误,缺少连接词导致句子不通顺。

建议修改:

-**Keep your tone light, friendly and curious**: add small touches of personality in preambles feel collaborative and engaging.
+**Keep your tone light, friendly and curious**: add small touches of personality so preambles feel collaborative and engaging.
internal/converter/codex_instructions/gpt_5_1_prompt.md-004-e0fb3ca1dbea0c418cf8b3c7b76ed671d62147e3-193-193 (1)

193-193: ⚠️ Potential issue | 🟡 Minor

语法错误:"If the completing" 应改为 "If completing"。

📝 建议修复
-Although they introduce friction to the user because your work is paused until the user responds, you should leverage them when necessary to accomplish important work. If the completing the task requires escalated permissions, Do not let these settings or the sandbox deter you from attempting to accomplish the user's task unless it is set to "never", in which case never ask for approvals.
+Although they introduce friction to the user because your work is paused until the user responds, you should leverage them when necessary to accomplish important work. If completing the task requires escalated permissions, do not let these settings or the sandbox deter you from attempting to accomplish the user's task unless it is set to "never", in which case never ask for approvals.

注:同时将 "Do not" 改为小写 "do not",因为它不是句首。

internal/converter/codex_instructions/gpt_5_1_prompt.md-002-8dcbd29edd5f204d47efa06560981cd089d21f7b-193-193 (1)

193-193: ⚠️ Potential issue | 🟡 Minor

改进审批策略"never"模式的表述

这句话使用了双重否定和复杂条件:"不要让这些设置或沙箱阻止你尝试完成用户任务,除非它设置为'never',在这种情况下永远不要请求审批。" 这种表述可能导致对关键审批流程的误解。

建议重写为更直接的表述,例如:"当 approval_policy 设置为 'never' 时,你绝不能请求用户审批;对于其他审批模式,不要让沙箱限制阻止你尝试完成任务。"

internal/converter/codex_instructions/gpt_5_1_prompt.md-002-8dcbd29edd5f204d47efa06560981cd089d21f7b-73-73 (1)

73-73: ⚠️ Potential issue | 🟡 Minor

澄清计划项批量完成的指导

第 73 行指出"不要事后批量完成多个项目",但第 368 行又说"可以在单次 update_plan 调用中标记多个项目为完成"。虽然这两条可能分别针对工作流程(不能跳过 in_progress 状态)和 API 能力(一次调用可以更新多个状态),但这种表述可能导致混淆。

建议在第 73 行或第 368 行添加说明,明确区分"不要跳过 in_progress 状态直接批量标记完成"与"可以在一次调用中将多个已经历 in_progress 的项目标记为完成"。

Also applies to: 368-368

internal/converter/codex_instructions/gpt-5.1-codex-max_prompt.md-001-d5dfba250975b4519fed9b8abf99bbd6c31e6f33-1-1 (1)

1-1: ⚠️ Potential issue | 🟡 Minor

模型版本表述与文件名不一致
Line 1 写的是 “GPT-5”,但文件名指向 gpt-5.1-codex...,容易造成指令目标不一致。建议对齐版本号。

建议修改
-You are Codex, based on GPT-5. You are running as a coding agent in the Codex CLI on a user's computer.
+You are Codex, based on GPT-5.1. You are running as a coding agent in the Codex CLI on a user's computer.
internal/converter/codex_instructions/gpt-5.1-codex-max_prompt.md-001-d5dfba250975b4519fed9b8abf99bbd6c31e6f33-37-57 (1)

37-57: ⚠️ Potential issue | 🟡 Minor

默认网络配置表述易歧义
Line 57 的 “network sandboxing enabled” 不对应上文枚举值 restricted/enabled,可能被解读成相反含义。建议使用枚举值直述。

建议修改
-If you are not told about this, assume that you are running with workspace-write, network sandboxing enabled, and approval on-failure.
+If you are not told about this, assume that you are running with `sandbox_mode == workspace-write`, `network_access == enabled`, and `approval_policy == on-failure`.
internal/converter/openai_to_claude_response.go-74-82 (1)

74-82: ⚠️ Potential issue | 🟡 Minor

确保 StopReason 不依赖 Message 是否为 nil

FinishReason 的映射放在 choice.Message != nil 的分支里,若上游返回 message: null 会导致 StopReason 为空。建议把映射挪到分支之外。

🔧 建议调整
 	if len(resp.Choices) > 0 {
 		choice := resp.Choices[0]
 		if choice.Message != nil {
 			// Convert reasoning_content to thinking blocks
 			if reasoningText := collectReasoningText(choice.Message.ReasoningContent); strings.TrimSpace(reasoningText) != "" {
 				claudeResp.Content = append(claudeResp.Content, ClaudeContentBlock{
 					Type:     "thinking",
 					Thinking: reasoningText,
 				})
 			}
 
 			// Convert content
 			switch content := choice.Message.Content.(type) {
 			case string:
 				if content != "" {
 					claudeResp.Content = append(claudeResp.Content, ClaudeContentBlock{
 						Type: "text",
 						Text: content,
 					})
 				}
 			case []interface{}:
 				for _, part := range content {
 					if m, ok := part.(map[string]interface{}); ok {
 						if m["type"] == "text" {
 							if text, ok := m["text"].(string); ok && text != "" {
 								claudeResp.Content = append(claudeResp.Content, ClaudeContentBlock{
 									Type: "text",
 									Text: text,
 								})
 							}
 						}
 					}
 				}
 			}
 
 			// Convert tool calls
 			for _, tc := range choice.Message.ToolCalls {
 				var input interface{}
 				json.Unmarshal([]byte(tc.Function.Arguments), &input)
 				claudeResp.Content = append(claudeResp.Content, ClaudeContentBlock{
 					Type:  "tool_use",
 					ID:    tc.ID,
 					Name:  tc.Function.Name,
 					Input: input,
 				})
 			}
-
-			// Map finish reason
-			switch choice.FinishReason {
-			case "stop":
-				claudeResp.StopReason = "end_turn"
-			case "length":
-				claudeResp.StopReason = "max_tokens"
-			case "tool_calls":
-				claudeResp.StopReason = "tool_use"
-			}
 		}
+		// Map finish reason
+		switch choice.FinishReason {
+		case "stop":
+			claudeResp.StopReason = "end_turn"
+		case "length":
+			claudeResp.StopReason = "max_tokens"
+		case "tool_calls":
+			claudeResp.StopReason = "tool_use"
+		}
 	}
internal/converter/gemini_to_claude_stream.go-11-14 (1)

11-14: ⚠️ Potential issue | 🟡 Minor

尽早处理 UsageMetadata,避免 message_delta 使用过时的 token 计数。

在第 134-158 行的 FinishReason 块中发送 message_delta 时,state.Usage.OutputTokens 可能是旧值。若当前 chunk 同时包含 FinishReasonUsageMetadata,应先更新 usage,再构造 message_delta。将第 161-164 行的 UsageMetadata 处理移至 FinishReason 检查之前。

internal/converter/codex_instructions.go-126-137 (1)

126-137: ⚠️ Potential issue | 🟡 Minor

模型名称匹配顺序可能导致意外行为。

当前匹配顺序可能导致问题。例如,模型名 "gpt-5.1-codex" 会匹配到 "codex" 分支(Line 130)而不是 "5.1" 分支(Line 132),因为 "codex" 检查在 "5.1" 之前。

建议调整匹配顺序,将更具体的模式放在前面:

♻️ 建议的改进
-if strings.Contains(modelName, "codex-max") {
+// 按特异性从高到低排序
+if strings.Contains(modelName, "5.2-codex") {
+  return false, last52CodexPrompt
+} else if strings.Contains(modelName, "codex-max") {
   return false, lastCodexMaxPrompt
-} else if strings.Contains(modelName, "5.2-codex") {
-  return false, last52CodexPrompt
 } else if strings.Contains(modelName, "codex") {
   return false, lastCodexPrompt
 } else if strings.Contains(modelName, "5.1") {
   return false, last51Prompt
 } else if strings.Contains(modelName, "5.2") {
   return false, last52Prompt
 }
internal/converter/codex_instructions/gpt_5_codex_prompt.md-009-e0fb3ca1dbea0c418cf8b3c7b76ed671d62147e3-85-85 (1)

85-85: ⚠️ Potential issue | 🟡 Minor

文本中存在拼写错误

"The user does not command execution outputs" 应该是 "The user does not see command execution outputs"。

📝 建议的修复
-- The user does not command execution outputs. When asked to show the output of a command (e.g. `git show`), relay the important details in your answer or summarize the key lines so the user understands the result.
+- The user does not see command execution outputs. When asked to show the output of a command (e.g. `git show`), relay the important details in your answer or summarize the key lines so the user understands the result.
internal/converter/gemini_to_openai.go-381-386 (1)

381-386: ⚠️ Potential issue | 🟡 Minor

splitFunctionName 函数返回的 callID 不正确

当前实现返回 name[idx+1:],这意味着如果 name"myTool_call_123",返回的 callID 将是 "call_123" 而不是 "123"。这可能不是预期行为。

🐛 建议的修复
 func splitFunctionName(name string) (string, string) {
 	if idx := strings.LastIndex(name, "_call_"); idx > 0 {
-		return name[:idx], name[idx+1:]
+		return name[:idx], name[idx+6:] // "_call_" 长度为 6
 	}
 	return name, ""
 }
internal/converter/gemini_to_openai.go-196-206 (1)

196-206: ⚠️ Potential issue | 🟡 Minor

InlineData 处理中的类型断言可能失败

在 Line 200,msg.Content.([]OpenAIContentPart) 的类型断言未检查 ok。如果 msg.Content 已被设置为其他类型(如 string),这里会 panic。

🛡️ 建议的修复
 			if part.InlineData != nil && part.InlineData.Data != "" {
 				if msg.Content == nil {
 					msg.Content = []OpenAIContentPart{}
 				}
-				parts, _ := msg.Content.([]OpenAIContentPart)
+				parts, ok := msg.Content.([]OpenAIContentPart)
+				if !ok {
+					// 如果已经是 string,转换为 parts
+					if s, ok := msg.Content.(string); ok {
+						parts = []OpenAIContentPart{{Type: "text", Text: s}}
+					}
+				}
 				parts = append(parts, OpenAIContentPart{
internal/converter/claude_to_gemini_response.go-31-38 (1)

31-38: ⚠️ Potential issue | 🟡 Minor

tool_use 输入类型断言失败会丢参
block.Input 不是 map[string]interface{} 时会静默生成空参数,转换结果缺参。建议显式报错(或回退解析)。

🛠️ 建议修复
-import "encoding/json"
+import (
+	"encoding/json"
+	"fmt"
+)
@@
-		case "tool_use":
-			inputMap, _ := block.Input.(map[string]interface{})
+		case "tool_use":
+			inputMap, ok := block.Input.(map[string]interface{})
+			if !ok {
+				return nil, fmt.Errorf("tool_use input must be object")
+			}
 			candidate.Content.Parts = append(candidate.Content.Parts, GeminiPart{
 				FunctionCall: &GeminiFunctionCall{
 					Name: block.Name,
 					Args: inputMap,
 					ID:   block.ID,
 				},
 			})
internal/converter/codex_instructions/gpt_5_codex_prompt.md-006-0ad1b0782b16bb5e91065da622b7c605d7d512e6-86-86 (1)

86-86: ⚠️ Potential issue | 🟡 Minor

文档表述疑似有误(与其他变体相同)

"The user does not command execution outputs" 应该是 "The user does not see command execution outputs"。建议在合并前统一修复所有提示文件变体中的此问题。

internal/converter/codex_instructions/gpt_5_codex_prompt.md-008-daf77b845230c35c325500ff73fe72a78f3b7416-85-85 (1)

85-85: ⚠️ Potential issue | 🟡 Minor

文档表述疑似有误(与其他变体相同)

"The user does not command execution outputs" 应该是 "The user does not see command execution outputs"。此问题在多个提示文件变体中存在。

internal/converter/codex_instructions/gpt_5_codex_prompt.md-002-c9505488a120299b339814d73f57817ee79e114f-84-84 (1)

84-84: ⚠️ Potential issue | 🟡 Minor

文档表述疑似有误

"The user does not command execution outputs" 这句话语义不通顺,可能应该是 "The user does not see command execution outputs"。

📝 建议修改
-- The user does not command execution outputs. When asked to show the output of a command (e.g. `git show`), relay the important details in your answer or summarize the key lines so the user understands the result.
+- The user does not see command execution outputs. When asked to show the output of a command (e.g. `git show`), relay the important details in your answer or summarize the key lines so the user understands the result.
internal/converter/coverage_gemini_response_test.go-95-98 (1)

95-98: ⚠️ Potential issue | 🟡 Minor

类型断言可能导致 panic

item["output"].(string)output 不是字符串类型时会 panic。建议使用带检查的类型断言。

🛡️ 建议的安全类型断言
 	item, ok := items[0].(map[string]interface{})
-	if !ok || !strings.Contains(item["output"].(string), "\"a\":1") {
+	outputStr, isStr := item["output"].(string)
+	if !ok || !isStr || !strings.Contains(outputStr, "\"a\":1") {
 		t.Fatalf("expected result object output")
 	}
internal/converter/codex_instructions/gpt_5_codex_prompt.md-005-35c76ad47d0f6f134923026c9c80d1f2e9bbd83f-84-84 (1)

84-84: ⚠️ Potential issue | 🟡 Minor

文档中可能存在拼写错误。

"The user does not command execution outputs" 似乎应该是 "The user does not see command execution outputs"。

📝 建议的修复
-- The user does not command execution outputs. When asked to show the output of a command (e.g. `git show`), relay the important details in your answer or summarize the key lines so the user understands the result.
+- The user does not see command execution outputs. When asked to show the output of a command (e.g. `git show`), relay the important details in your answer or summarize the key lines so the user understands the result.
internal/converter/claude_to_gemini_branches2_test.go-113-123 (1)

113-123: ⚠️ Potential issue | 🟡 Minor

测试断言逻辑过于宽松。

当前断言逻辑 p.Text != "" && p.Text != "..." 会对任何非空且不是 "..." 的文本返回 true,这并不能有效验证 redacted_thinking 的处理是否正确。

建议改进断言,更精确地验证预期的输出内容:

💡 建议的改进
 	foundRedacted := false
 	for _, c := range got.Contents {
 		for _, p := range c.Parts {
-			if p.Text != "" && p.Text != "..." {
+			// 验证 redacted_thinking 被正确处理
+			// 根据实际实现调整预期文本内容
+			if strings.Contains(p.Text, "redacted") || strings.Contains(p.Text, "secret") {
 				foundRedacted = true
 			}
 		}
 	}

需要根据 claudeToGeminiRequest.Transform 的实际实现来确定 redacted_thinking 块应该被转换成什么内容。

internal/converter/gemini_to_claude_response.go-16-20 (1)

16-20: ⚠️ Potential issue | 🟡 Minor

ClaudeResponse 中 Model 字段未设置。

ClaudeResponse 结构体包含 Model 字段,但在转换过程中从未被赋值。这可能导致响应中 model 字段为空字符串。

另外,ID 字段硬编码为 "msg_gemini" 可能不满足需要唯一消息标识的场景。

💡 建议的改进
+import (
+	"encoding/json"
+	"fmt"
+	"crypto/rand"
+	"encoding/hex"
+)
+
+func generateMessageID() string {
+	b := make([]byte, 12)
+	rand.Read(b)
+	return "msg_" + hex.EncodeToString(b)
+}
+
 func (c *geminiToClaudeResponse) Transform(body []byte) ([]byte, error) {
 	var resp GeminiResponse
 	if err := json.Unmarshal(body, &resp); err != nil {
 		return nil, err
 	}

 	claudeResp := ClaudeResponse{
-		ID:   "msg_gemini",
+		ID:   generateMessageID(),
 		Type: "message",
 		Role: "assistant",
+		Model: "gemini", // 或者从请求上下文传入实际模型名称
 	}

如果需要传递实际的模型名称,可以考虑修改 Transform 方法签名以接收模型参数。

internal/converter/gemini_to_claude_helpers.go-15-52 (1)

15-52: ⚠️ Potential issue | 🟡 Minor

始终删除 query 键,避免 Claude Code 工具调用失败。

Claude Code 的 grep/glob 工具使用严格的模式验证,仅接受 patternpath 等明确定义的字段。当 pattern 已存在时,当前代码未删除 query,导致工具接收不被允许的字段,将引发 "Extra inputs are not permitted" 错误。建议无论 pattern 是否存在,都应删除 query

建议修改
case "grep":
	// Gemini uses "query", Claude Code expects "pattern"
	if query, ok := args["query"]; ok {
		if _, hasPattern := args["pattern"]; !hasPattern {
			args["pattern"] = query
-			delete(args, "query")
		}
+		delete(args, "query")
	}
	// Claude Code uses "path" (string), NOT "paths" (array)
	if _, hasPath := args["path"]; !hasPath {
		if paths, ok := args["paths"]; ok {
			pathStr := extractFirstPath(paths)
			args["path"] = pathStr
			delete(args, "paths")
		} else {
			args["path"] = "."
		}
	}

case "glob":
	// Gemini uses "query", Claude Code expects "pattern"
	if query, ok := args["query"]; ok {
		if _, hasPattern := args["pattern"]; !hasPattern {
			args["pattern"] = query
-			delete(args, "query")
		}
+		delete(args, "query")
	}
	// Claude Code uses "path" (string), NOT "paths" (array)
	if _, hasPath := args["path"]; !hasPath {
		if paths, ok := args["paths"]; ok {
			pathStr := extractFirstPath(paths)
			args["path"] = pathStr
			delete(args, "paths")
		} else {
			args["path"] = "."
		}
	}
internal/converter/codex_instructions/gpt_5_codex_prompt.md-004-5d78c1edd337c038a1207c30fe8a6fa329e3d502-85-85 (1)

85-85: ⚠️ Potential issue | 🟡 Minor

与 -003 文件相同的文本笔误。

同上一个提示文件,第 85 行 "The user does not command execution outputs" 应修改为 "does not see"。

internal/converter/codex_instructions/gpt_5_codex_prompt.md-003-f6a152848a09943089dcb9cb90de086e58008f2a-85-85 (1)

85-85: ⚠️ Potential issue | 🟡 Minor

文档文本可能存在笔误。

第 85 行 "The user does not command execution outputs" 语义不通顺,可能应为 "The user does not see command execution outputs" 或类似表述。

📝 建议的修复
-- The user does not command execution outputs. When asked to show the output of a command (e.g. `git show`), relay the important details in your answer or summarize the key lines so the user understands the result.
+- The user does not see command execution outputs. When asked to show the output of a command (e.g. `git show`), relay the important details in your answer or summarize the key lines so the user understands the result.
🧹 Nitpick comments (35)
internal/converter/claude_codex_stream_test.go (1)

5-11: 建议验证 TransformChunk 的返回值

当前测试仅检查是否有错误返回,但忽略了 TransformChunk 的实际输出结果。为了提高测试覆盖质量,可以考虑对返回的字节切片进行断言,确保流式 "done" 事件被正确转换。

♻️ 可选改进示例
 func TestClaudeToCodexResponse_StreamDoneEvent(t *testing.T) {
 	conv := &claudeToCodexResponse{}
 	state := NewTransformState()
-	if _, err := conv.TransformChunk(FormatDone(), state); err != nil {
+	result, err := conv.TransformChunk(FormatDone(), state)
+	if err != nil {
 		t.Fatalf("TransformChunk done: %v", err)
 	}
+	// 根据预期行为验证 result,例如:
+	// if len(result) == 0 { ... } 或检查特定的输出格式
 }
internal/converter/codex_instructions/gpt_5_2_prompt.md-001-238ce7dfad3916c325d9919a829ecd5ce60ef43a (2)

39-43: 建议统一列表项的标点符号风格。

第 42 行缺少句末句号,而同一段落中的其他描述性文本都有句号。建议保持一致性。

📝 建议修改
-  - Only the initial plan, plan updates, and final recap can be longer, with multiple bullets and paragraphs
+  - Only the initial plan, plan updates, and final recap can be longer, with multiple bullets and paragraphs.

75-84: 列表项标点符号不一致。

第 81 和 83 行的列表项缺少句末句号,而第 77-80 和 82 行的列表项都有句号。建议统一格式。

📝 建议修改
-- When the user asked you to do more than one thing in a single prompt
+- When the user asked you to do more than one thing in a single prompt.
 - The user has asked you to use the plan tool (aka "TODOs")
-- You generate additional steps while working, and plan to do them before yielding to the user
+- You generate additional steps while working, and plan to do them before yielding to the user.
internal/converter/codex_instructions/gpt_5_1_prompt.md-004-e0fb3ca1dbea0c418cf8b3c7b76ed671d62147e3 (1)

63-134: 计划示例对比有效地说明了质量标准。

高质量和低质量计划示例的对比非常直观,能够帮助理解什么是好的任务分解。建议在低质量示例中简要说明为什么这些计划不够好(例如:步骤过于笼统、缺乏可验证的检查点等),这样指导性会更强。

internal/adapter/client/adapter_test.go (1)

11-26: 测试逻辑正确,建议补充端点检测场景的测试覆盖。

当前测试仅覆盖了 body 检测回退路径下的 User-Agent 覆盖逻辑。建议增加对端点路径检测的测试用例(如 /v1/messages/v1/responses 等),以确保完整的检测逻辑被覆盖。

💡 建议添加的测试用例
func TestDetectClientTypeByEndpoint(t *testing.T) {
	adapter := NewAdapter()
	body := []byte(`{"messages":[{"role":"user","content":"hi"}]}`)

	tests := []struct {
		name     string
		path     string
		wantType domain.ClientType
	}{
		{"Claude endpoint", "/v1/messages", domain.ClientTypeClaude},
		{"OpenAI endpoint", "/v1/chat/completions", domain.ClientTypeOpenAI},
		{"Codex endpoint", "/v1/responses", domain.ClientTypeCodex},
	}

	for _, tt := range tests {
		t.Run(tt.name, func(t *testing.T) {
			req := httptest.NewRequest("POST", tt.path, strings.NewReader(string(body)))
			if got := adapter.DetectClientType(req, body); got != tt.wantType {
				t.Fatalf("client type = %s, want %s", got, tt.wantType)
			}
		})
	}
}
internal/adapter/client/adapter.go (1)

35-56: MatchDetectClientType 存在重复的端点检测逻辑。

两个方法的 switch 语句几乎相同,这种重复已经导致了 /v1/responses 遗漏的问题。建议提取公共的端点检测逻辑以避免类似的不一致。

♻️ 重构建议:提取公共端点检测逻辑
// detectFromPath 从 URL 路径检测客户端类型
func (a *Adapter) detectFromPath(path string) (domain.ClientType, bool) {
    switch {
    case strings.HasPrefix(path, "/v1/messages"):
        return domain.ClientTypeClaude, true
    case strings.HasPrefix(path, "/responses"),
         strings.HasPrefix(path, "/v1/responses"):
        return domain.ClientTypeCodex, true
    case strings.HasPrefix(path, "/v1/chat/completions"):
        return domain.ClientTypeOpenAI, true
    case strings.HasPrefix(path, "/v1beta/models/"),
         strings.HasPrefix(path, "/v1internal/models/"):
        return domain.ClientTypeGemini, true
    }
    return "", false
}

然后 MatchDetectClientType 都可以复用此方法。

Also applies to: 224-247

internal/adapter/provider/custom/claude_body_test.go (1)

382-399: 建议添加更多边界情况测试。

当前测试覆盖了主要场景,但建议补充以下边界情况以提高覆盖率:

  • thinking 字段不存在
  • thinking.enabled 字段不存在
  • budget_tokensnull
♻️ 建议的额外测试用例
 func TestEnsureMinThinkingBudget(t *testing.T) {
+	// thinking field absent - should return unchanged
+	body := []byte(`{"model":"claude-3-5-sonnet"}`)
+	updated := ensureMinThinkingBudget(body)
+	if string(updated) != string(body) {
+		t.Fatalf("body without thinking should be unchanged")
+	}
+
 	body := []byte(`{"thinking":{"enabled":{"budget_tokens":512}}}`)
 	updated := ensureMinThinkingBudget(body)
internal/converter/codex_instructions/gpt-5.1-codex-max_prompt.md-001-d5dfba250975b4519fed9b8abf99bbd6c31e6f33 (1)

67-69: 建议补充“无发现时”的评审口径
当前强调“发现优先”,但未明确无问题时如何回应。建议补一句“无发现时需明确说明并提示残余风险/测试缺口”。

建议补充
-Findings must be the primary focus of the response - keep summaries or overviews brief and only after enumerating the issues.
+Findings must be the primary focus of the response - keep summaries or overviews brief and only after enumerating the issues. If no issues are found, explicitly state that and mention any residual risks or testing gaps.
internal/converter/codex_to_openai.go (1)

30-32: 建议规范化 Reasoning.Effort 再写入 ReasoningEffort
目前直接透传字符串,若输入含大小写/空白或非允许值可能导致 OpenAI 请求失败。建议 trim+lowercase 并按需校验允许值,与 codexToGemini 的处理保持一致;请核对 OpenAI 对 reasoning_effort 的合法取值。

♻️ 建议修改
 import (
 	"encoding/json"
+	"strings"
 	"time"

 	"github.com/awsl-project/maxx/internal/domain"
 )
@@
-	if req.Reasoning != nil && req.Reasoning.Effort != "" {
-		openaiReq.ReasoningEffort = req.Reasoning.Effort
-	}
+	if req.Reasoning != nil {
+		effort := strings.ToLower(strings.TrimSpace(req.Reasoning.Effort))
+		if effort != "" {
+			openaiReq.ReasoningEffort = effort
+		}
+	}
internal/converter/codex_to_gemini.go (1)

41-53: 确认 Gemini thinking 参数映射合法性
这里把 auto 映射为 thinkingBudget=-1,其他值直接写入 ThinkingLevel 并设置 IncludeThoughts。若 Gemini API 不接受 -1none,请求会失败。建议核对允许取值,并在 auto 时考虑省略预算字段或改用指针+omitempty,同时对未知值做校验/降级。

internal/converter/coverage_claude_stream_test.go (1)

151-167: 存在重复的测试用例。

以下测试函数测试的场景几乎相同(均测试 MAX_TOKENS finish reason):

  • TestGeminiToClaudeStreamMaxTokens (Line 151-167)
  • TestGeminiToClaudeStreamFinishStopsBlock (Line 257-273)
  • TestGeminiToClaudeStreamMaxTokensStop (Line 275-291)
  • TestGeminiToClaudeStreamTextFinishLength (Line 329-345)

建议合并这些测试或使用表驱动测试(table-driven tests)来减少代码重复,提高可维护性。

Also applies to: 257-273, 275-291, 329-345

internal/converter/claude_to_codex.go (1)

113-128: 默认 Reasoning 设置逻辑清晰,但顺序可优化。

当前逻辑:

  1. 如果有 OutputConfig.Effort,创建 Reasoning
  2. 调用 CodexInstructionsForModel
  3. 如果 Reasoning 为 nil,创建默认值
  4. 确保 Summary 和 Effort 有默认值

建议将默认值设置逻辑整合在一起以提高可读性:

♻️ 建议的重构
 if req.OutputConfig != nil {
   effort := strings.ToLower(strings.TrimSpace(req.OutputConfig.Effort))
-  codexReq.Reasoning = &CodexReasoning{Effort: effort}
+  codexReq.Reasoning = &CodexReasoning{Effort: effort, Summary: "auto"}
 }
+if codexReq.Reasoning == nil {
+  codexReq.Reasoning = &CodexReasoning{Effort: "medium", Summary: "auto"}
+} else {
+  if codexReq.Reasoning.Effort == "" {
+    codexReq.Reasoning.Effort = "medium"
+  }
+  if codexReq.Reasoning.Summary == "" {
+    codexReq.Reasoning.Summary = "auto"
+  }
+}
 _, instructions := CodexInstructionsForModel(model, "", userAgent)
 if GetCodexInstructionsEnabled() {
   codexReq.Instructions = instructions
 }
-if codexReq.Reasoning == nil {
-  codexReq.Reasoning = &CodexReasoning{Effort: "medium", Summary: "auto"}
-} else if codexReq.Reasoning.Summary == "" {
-  codexReq.Reasoning.Summary = "auto"
-}
-if codexReq.Reasoning.Effort == "" {
-  codexReq.Reasoning.Effort = "medium"
-}
internal/converter/claude_codex_test.go (1)

68-95: 流式测试仅验证无错误,建议增加输出内容断言。

TestCodexToClaudeResponse_Stream 测试仅检查 TransformChunk 调用是否返回错误,但未验证输出内容是否正确。建议添加对输出的断言:

♻️ 建议的改进
 func TestCodexToClaudeResponse_Stream(t *testing.T) {
   conv := &codexToClaudeResponse{}
   state := NewTransformState()

   created := map[string]interface{}{
     "type": "response.created",
     "response": map[string]interface{}{
       "id": "resp_1",
     },
   }
-  if _, err := conv.TransformChunk(FormatSSE("", created), state); err != nil {
+  out, err := conv.TransformChunk(FormatSSE("", created), state)
+  if err != nil {
     t.Fatalf("TransformChunk created: %v", err)
   }
+  if !strings.Contains(string(out), "message_start") {
+    t.Fatalf("expected message_start event")
+  }
   // ... 对其他事件也添加类似断言
 }
internal/converter/codex_instructions.go (2)

99-99: 静默忽略错误可能掩盖问题。

ReadDirReadFile 的错误被静默忽略。虽然嵌入的文件系统通常不会失败,但建议至少记录日志以便调试:

♻️ 建议的改进
 func codexInstructionsForCodex(modelName, systemInstructions string) (bool, string) {
-  entries, _ := codexInstructionsDir.ReadDir("codex_instructions")
+  entries, err := codexInstructionsDir.ReadDir("codex_instructions")
+  if err != nil {
+    // 嵌入文件系统不应失败,但记录以便调试
+    return false, ""
+  }
   // ...
   for _, entry := range entries {
-    content, _ := codexInstructionsDir.ReadFile("codex_instructions/" + entry.Name())
+    content, err := codexInstructionsDir.ReadFile("codex_instructions/" + entry.Name())
+    if err != nil {
+      continue
+    }

Also applies to: 108-108


98-138: 每次调用都读取所有嵌入文件,存在性能优化空间。

codexInstructionsForCodex 在每次调用时都会遍历并读取 codex_instructions 目录下的所有文件。考虑到这些是嵌入的静态文件,可以在 init() 或首次调用时缓存内容:

♻️ 建议的缓存方案
var (
    promptCache     map[string]string
    promptCacheOnce sync.Once
)

func loadPromptCache() {
    promptCacheOnce.Do(func() {
        promptCache = make(map[string]string)
        entries, _ := codexInstructionsDir.ReadDir("codex_instructions")
        for _, entry := range entries {
            content, _ := codexInstructionsDir.ReadFile("codex_instructions/" + entry.Name())
            promptCache[entry.Name()] = string(content)
        }
    })
}
internal/converter/openai_to_codex.go (1)

139-151: claude_to_codex.go 存在代码重复。

Lines 139-151 的指令和默认 Reasoning 设置逻辑与 claude_to_codex.go (Lines 117-128) 几乎相同。建议提取为共享辅助函数:

♻️ 建议的重构
// 可以在 codex_instructions.go 或新的 helpers 文件中定义
func ApplyCodexDefaults(codexReq *CodexRequest, model, userAgent string) {
    _, instructions := CodexInstructionsForModel(model, "", userAgent)
    if GetCodexInstructionsEnabled() {
        codexReq.Instructions = instructions
    }
    if codexReq.Reasoning == nil {
        codexReq.Reasoning = &CodexReasoning{Effort: "medium", Summary: "auto"}
    } else {
        if codexReq.Reasoning.Effort == "" {
            codexReq.Reasoning.Effort = "medium"
        }
        if codexReq.Reasoning.Summary == "" {
            codexReq.Reasoning.Summary = "auto"
        }
    }
}
internal/converter/codex_reasoning_test.go (2)

18-18: 忽略 json.Marshal 错误可能掩盖测试问题

测试中多处使用 body, _ := json.Marshal(req) 忽略了序列化错误。虽然在这些简单场景下不太可能失败,但明确检查错误可以让测试更健壮。

♻️ 建议的改进
-	body, _ := json.Marshal(req)
+	body, err := json.Marshal(req)
+	if err != nil {
+		t.Fatalf("marshal request: %v", err)
+	}

111-122: 复杂的类型断言链可读性较低

这段嵌套的类型断言逻辑虽然功能正确,但可读性不高。考虑提取为辅助函数或添加简短注释说明检查逻辑。

internal/converter/more_converter_extra_test.go (1)

152-154: 断言可以更具体

当前断言只检查 FinishReason 非空,但没有验证具体值。考虑验证预期的 finish reason 值以使测试更精确。

♻️ 建议的改进
-	if len(got.Candidates) == 0 || got.Candidates[0].FinishReason == "" {
-		t.Fatalf("expected finishReason, got %#v", got.Candidates)
+	if len(got.Candidates) == 0 {
+		t.Fatalf("expected candidates, got none")
+	}
+	// 当存在 tool_calls 时,Gemini 的 finishReason 通常为 "STOP"
+	if got.Candidates[0].FinishReason == "" {
+		t.Fatalf("expected finishReason to be set, got empty")
 	}
internal/converter/claude_to_gemini_helpers.go (3)

39-82: cleanJSONSchema 原地修改输入参数

此函数直接修改传入的 schema map,可能导致调用方的原始数据被意外修改。如果调用方期望保留原始 schema,这可能导致问题。

♻️ 建议:添加注释说明或考虑深拷贝
 // cleanJSONSchema recursively removes fields not supported by Gemini
 // Matches Antigravity-Manager's clean_json_schema function
+// NOTE: This function modifies the input map in place.
 func cleanJSONSchema(schema map[string]interface{}) {

129-140: hasValidThinkingSignature 对空白字符串的处理

Line 134 检查 thinking == "",但如果 thinking 只包含空白字符(如 " "),会被认为是有内容的。考虑是否需要使用 strings.TrimSpace

♻️ 建议的改进
 func hasValidThinkingSignature(block map[string]interface{}) bool {
 	sig, hasSig := block["signature"].(string)
 	thinking, _ := block["thinking"].(string)

 	// Empty thinking + any signature = valid (trailing signature case)
-	if thinking == "" && hasSig {
+	if strings.TrimSpace(thinking) == "" && hasSig {
 		return true
 	}

395-399: targetModelSupportsThinking 的逻辑可能过于宽松

当前实现对任何以 "claude-" 开头的模型都返回 true,但并非所有 Claude 模型都支持 thinking 模式。考虑添加更精确的模型列表检查。

internal/converter/gemini_to_claude_response_test.go (1)

36-40: 类型断言可以更安全

连续的类型断言 got.Content[0].Input.(map[string]interface{}) 如果 Input 不是预期类型会导致 panic。考虑添加检查或使用 comma-ok 模式。

♻️ 建议的改进
+	input, ok := got.Content[0].Input.(map[string]interface{})
+	if !ok {
+		t.Fatalf("expected Input to be map[string]interface{}, got %T", got.Content[0].Input)
+	}
-	if _, ok := got.Content[0].Input.(map[string]interface{})["pattern"]; !ok {
+	if _, ok := input["pattern"]; !ok {
 		t.Fatalf("expected pattern remap")
 	}
-	if _, ok := got.Content[0].Input.(map[string]interface{})["path"]; !ok {
+	if _, ok := input["path"]; !ok {
 		t.Fatalf("expected path remap")
 	}
internal/converter/claude_to_gemini_full_test.go (1)

61-71: 更严格校验工具结果字段
当前仅在 result 为 string 时才断言内容,字段缺失或类型不匹配会静默通过,容易掩盖回归。建议显式断言结构并在异常时失败。

♻️ 建议改动
-				if respMap, ok := p.FunctionResponse.Response.(map[string]interface{}); ok {
-					if result, ok := respMap["result"].(string); ok {
-						if !strings.Contains(result, "ok") {
-							t.Fatalf("unexpected tool result")
-						}
-					}
-				}
+				respMap, ok := p.FunctionResponse.Response.(map[string]interface{})
+				if !ok {
+					t.Fatalf("expected function response map")
+				}
+				result, ok := respMap["result"].(string)
+				if !ok {
+					t.Fatalf("expected function response result string")
+				}
+				if !strings.Contains(result, "ok") {
+					t.Fatalf("unexpected tool result")
+				}
internal/converter/claude_to_openai_response.go (1)

37-43: json.Marshal 错误被静默忽略

block.Input 无法序列化时,错误会被丢弃,inputJSON 将为 nil,导致 Arguments 字段变为 "null" 字符串。虽然 block.Input 通常是 map[string]interface{},理论上应该总是可序列化的,但显式处理错误可以提高代码健壮性并便于调试。

♻️ 建议的修复方案
 		case "tool_use":
-			inputJSON, _ := json.Marshal(block.Input)
+			inputJSON, err := json.Marshal(block.Input)
+			if err != nil {
+				// 作为回退,使用空对象
+				inputJSON = []byte("{}")
+			}
 			toolCalls = append(toolCalls, OpenAIToolCall{
internal/converter/coverage_gemini_response_test.go (1)

119-122: interface{} 与空字符串的比较可能不符合预期

item["output"] == "" 仅当 output 恰好是空字符串类型时才为 true。如果 outputnil 或其他类型,此比较将返回 false,测试可能会通过但实际上输出不正确。

♻️ 更健壮的检查方式
 	item, ok := items[0].(map[string]interface{})
-	if !ok || item["output"] == "" {
+	outputStr, isStr := item["output"].(string)
+	if !ok || !isStr || outputStr == "" {
 		t.Fatalf("expected string output")
 	}
internal/converter/coverage_misc_validation_test.go (1)

7-66: 测试结构可以改进以提升可调试性

当前测试在结构体初始化时执行 Transform 调用,并在循环中检查错误。建议使用 t.Run 子测试,这样当某个转换器失败时,测试输出会更清晰地指出是哪个失败了。

♻️ 建议的改进方案
 func TestValidation_InvalidJSONRequests(t *testing.T) {
-	reqs := []struct {
+	testCases := []struct {
 		name string
-		err  error
+		fn   func() error
 	}{
-		{"claude_to_gemini", func() error {
-			_, err := (&claudeToGeminiRequest{}).Transform([]byte("{"), "gemini", false)
-			return err
-		}()},
+		{"claude_to_gemini", func() error {
+			_, err := (&claudeToGeminiRequest{}).Transform([]byte("{"), "gemini", false)
+			return err
+		}},
 		// ... 其他测试用例类似修改 ...
 	}
-	for _, item := range reqs {
-		if item.err == nil {
-			t.Fatalf("expected error for %s", item.name)
-		}
+	for _, tc := range testCases {
+		t.Run(tc.name, func(t *testing.T) {
+			if err := tc.fn(); err == nil {
+				t.Errorf("expected error for %s", tc.name)
+			}
+		})
 	}
 }
internal/converter/coverage_claude_helpers_test.go (2)

86-89: nil 安全测试缺少断言。

这个测试只验证调用 applyClaudeThinkingToOpenAI 时传入 nil 参数不会 panic,但没有验证返回值或状态。虽然这对于防止 panic 是有用的,但考虑添加断言来验证 nil 输入时的预期行为。

💡 可选的改进
 func TestApplyClaudeThinkingNilCases(t *testing.T) {
+    // 验证 nil 输入不会 panic
     applyClaudeThinkingToOpenAI(nil, &ClaudeRequest{})
     applyClaudeThinkingToOpenAI(&OpenAIRequest{}, nil)
+    
+    // 验证 nil claudeReq 不会修改 openaiReq
+    openaiReq := &OpenAIRequest{}
+    applyClaudeThinkingToOpenAI(openaiReq, nil)
+    if openaiReq.ReasoningEffort != "" {
+        t.Fatalf("expected empty reasoning effort for nil claudeReq")
+    }
 }

115-119: 仅覆盖率测试 - 可接受但建议添加断言。

第 115-119 行调用 RemoveTrailingUnsignedThinking 但没有验证结果。虽然这确保了代码路径被执行,但添加断言会使测试更有价值。

internal/converter/claude_to_openai_helpers.go (2)

81-97: budget 阈值使用魔法数字。

mapBudgetToEffort 中的阈值(1024、8192)缺少解释。建议提取为命名常量或添加注释说明这些值的来源和含义。

♻️ 建议的重构
+const (
+    budgetThresholdLow    = 1024  // tokens below this are "low" effort
+    budgetThresholdMedium = 8192  // tokens below this are "medium" effort
+)
+
 func mapBudgetToEffort(budget int) string {
     switch {
     case budget < 0:
         if budget == -1 {
             return "auto"
         }
         return ""
     case budget == 0:
         return "none"
-    case budget <= 1024:
+    case budget <= budgetThresholdLow:
         return "low"
-    case budget <= 8192:
+    case budget <= budgetThresholdMedium:
         return "medium"
     default:
         return "high"
     }
 }

68-79: int64 转 int 可能溢出。

在 32 位系统上,int64 转换为 int 可能导致溢出。虽然在实际使用中 budget 值不太可能超出 int32 范围,但建议添加边界检查或文档说明。

internal/converter/codex_instructions/gpt_5_codex_prompt.md-004-5d78c1edd337c038a1207c30fe8a6fa329e3d502 (1)

1-104: 与 gpt_5_codex_prompt.md-003 内容重复。

此文件与 -003 变体内容几乎完全相同。如果这是有意的版本控制策略,建议添加版本标识或变更日志说明差异;如果是无意重复,考虑合并以减少维护负担。

internal/converter/coverage_gemini_helpers_test.go (1)

450-451: geminiToCodexRequest.Transform 中为 Stream 默认值添加说明注释。

验证确认:即使 Transform 调用时传入 stream: false,代码仍然无条件地将 codexReq.Stream 设置为 true。这是有意的设计决策(Codex 总是使用流式传输),但 gemini_to_codex.go 中的 codexReq.Stream = true 赋值处缺少说明注释。建议在此行上方添加注释,解释为什么忽略流参数并总是强制使用流式传输。

internal/converter/openai_to_claude_request.go (1)

57-65: 工具结果内容不应仅限 string。
OpenAI 的 tool 消息内容可能是数组或对象,直接断言 string 会丢失信息;建议复用已有的 stringifyContent

♻️ 建议改法
-            contentStr, _ := msg.Content.(string)
+            contentStr := stringifyContent(msg.Content)
internal/converter/claude_to_gemini_request.go (1)

80-82: 请不要在关键路径遗留 TODO:签名缓存尚未接入。
Line 80 这里的 TODO 暗示仍依赖外部签名缓存才能完整校验。建议在合并前补上实现或至少用 issue/注释明确为什么当前行为可接受。

如果需要,我可以帮你起草最小可用的签名缓存接口或补充测试用例。

Comment on lines +179 to +186
for _, tc := range msg.ToolCalls {
var args map[string]interface{}
json.Unmarshal([]byte(tc.Function.Arguments), &args)
geminiContent.Parts = append(geminiContent.Parts, GeminiPart{
FunctionCall: &GeminiFunctionCall{
Name: tc.Function.Name,
Args: args,
},
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

工具调用参数解析错误需显式处理。
json.Unmarshal 失败后 Args 会变成 nil,但仍发送 functionCall,容易造成下游失败且难排查;建议返回错误。

🐛 修复建议(显式处理 JSON 解析失败)
-            var args map[string]interface{}
-            json.Unmarshal([]byte(tc.Function.Arguments), &args)
+            var args map[string]interface{}
+            if err := json.Unmarshal([]byte(tc.Function.Arguments), &args); err != nil {
+                return nil, err
+            }
             geminiContent.Parts = append(geminiContent.Parts, GeminiPart{
                 FunctionCall: &GeminiFunctionCall{
                     Name: tc.Function.Name,
                     Args: args,
                 },
                 ThoughtSignature: geminiFunctionThoughtSignature,
             })
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
for _, tc := range msg.ToolCalls {
var args map[string]interface{}
json.Unmarshal([]byte(tc.Function.Arguments), &args)
geminiContent.Parts = append(geminiContent.Parts, GeminiPart{
FunctionCall: &GeminiFunctionCall{
Name: tc.Function.Name,
Args: args,
},
for _, tc := range msg.ToolCalls {
var args map[string]interface{}
if err := json.Unmarshal([]byte(tc.Function.Arguments), &args); err != nil {
return nil, err
}
geminiContent.Parts = append(geminiContent.Parts, GeminiPart{
FunctionCall: &GeminiFunctionCall{
Name: tc.Function.Name,
Args: args,
},
🤖 Prompt for AI Agents
In `@internal/converter/openai_to_gemini_request.go` around lines 179 - 186, The
loop that converts msg.ToolCalls builds GeminiPart.FunctionCall by unmarshaling
tc.Function.Arguments with json.Unmarshal but ignores the error, which can
produce nil Args and a bad functionCall; update the conversion (the code that
appends to geminiContent.Parts / constructs GeminiPart and GeminiFunctionCall)
to check the json.Unmarshal error for each tc, and if unmarshaling fails return
or propagate a descriptive error (including the function name tc.Function.Name
and the raw arguments string) instead of appending a function call with nil Args
so downstream callers don't receive invalid data.

Bowl42 and others added 3 commits February 1, 2026 19:05
The ensureMinThinkingBudget function was checking `thinking.enabled.budget_tokens`
but the Claude API format is `thinking.budget_tokens` with a sibling `thinking.type`
field. This caused the 1024 minimum guard to never trigger.

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
Address PR review comments: add nil-pointer checks for state.Usage
and state.ToolCalls, handle json.Unmarshal errors instead of silently
ignoring them, treat OpenAI "developer" role as system for Claude,
respect stream parameter in gemini-to-codex, fix "none" ThinkingLevel
for Gemini API, use FunctionResponse.ID for tool_result association,
remove incorrect cleanJSONSchema on tool call arguments, and skip
tool blocks with empty name/id.

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
Fix patch envelope markers from **_/_** to standard ***, change
apply_patch JSON field from "cmd" to "command", and correct line
prefix descriptions to use +/- instead of -/*.

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 7

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
internal/converter/gemini_to_codex.go (1)

94-159: ⚠️ Potential issue | 🟠 Major

文本与工具调用的顺序可能被打乱。
当前在同一条 content 同时包含文本与 FunctionCall/FunctionResponse 时,文本会在循环末尾统一追加,导致文本被放到调用之后并被合并,顺序语义可能改变。建议在追加 function_call / function_call_output 之前先 flush 已累计的 contentParts 并清空。

🛠️ 建议修复
@@
 			if part.FunctionCall != nil {
+				if len(contentParts) > 0 {
+					inputItems = append(inputItems, map[string]interface{}{
+						"type":    "message",
+						"role":    role,
+						"content": contentParts,
+					})
+					contentParts = nil
+				}
 				argsJSON, _ := json.Marshal(part.FunctionCall.Args)
 				// Extract call_id from name if present
 				name := part.FunctionCall.Name
@@
 			if part.FunctionResponse != nil {
+				if len(contentParts) > 0 {
+					inputItems = append(inputItems, map[string]interface{}{
+						"type":    "message",
+						"role":    role,
+						"content": contentParts,
+					})
+					contentParts = nil
+				}
 				callID := ""
 				if len(pendingCallIDs) > 0 {
 					callID = pendingCallIDs[0]
🤖 Fix all issues with AI agents
In `@internal/converter/claude_to_gemini_stream.go`:
- Around line 20-33: The switch handling for claudeEvent.Type currently
processes only "content_block_delta" with a "text_delta"
(claudeEvent.Delta.Type) and drops "thinking_delta"; update the same branch in
claude_to_gemini_stream.go to detect when claudeEvent.Delta.Type ==
"thinking_delta" and emit a GeminiStreamChunk identical in shape to the text
case (use GeminiStreamChunk, GeminiCandidate, GeminiContent with Role:"model"
and Parts containing an empty or placeholder GeminiPart as done for text), then
append the result via FormatSSE just like the existing logic so
TestClaudeToGeminiResponse_StreamThinking passes and behavior matches
claude_to_openai_stream.go handling of "thinking_delta".
- Around line 35-48: The code updates state.Usage.OutputTokens in the
"message_delta" case but never sets state.Usage.InputTokens, so PromptTokenCount
stays zero; add handling for the "message_start" event to initialize state.Usage
(if nil) and set state.Usage.InputTokens from the incoming claudeEvent usage
payload (e.g., claudeEvent.Usage.InputTokens or claudeEvent.InputTokens as
appropriate), mirroring the pattern used in "message_delta" to ensure
state.Usage.InputTokens is populated before "message_stop" reads it.

In `@internal/converter/openai_to_claude_request.go`:
- Around line 79-99: The conversion of OpenAI message parts in the switch over
msg.Content currently only handles string and type="text" and silently drops
other part types; update the logic in the switch (the block that appends
ClaudeContentBlock items) to explicitly handle partType=="image_url" by creating
a ClaudeContentBlock representing an image (use the URL or fetch+base64 as
appropriate for Claude's expected image format), and for unsupported part types
like "input_audio" and "file" emit an explicit error or log warning instead of
silently ignoring them; update references to ClaudeContentBlock, msg.Content,
and the inner partType switch to implement this behavior.
- Around line 100-112: The loop handling msg.ToolCalls constructs
ClaudeContentBlock entries of Type "tool_use" but doesn't validate the required
fields; add explicit non-empty checks for tc.ID and tc.Function.Name before
appending and return an error if either is empty so callers surface upstream
data problems. Update the for loop that iterates msg.ToolCalls to validate tc.ID
and tc.Function.Name, and if either is blank return a descriptive error
(mentioning "tool_use id" or "tool_use name") instead of silently proceeding;
keep the json.Unmarshal of tc.Function.Arguments and the subsequent creation of
ClaudeContentBlock unchanged aside from this validation.
- Around line 34-53: The loop over req.Messages currently overwrites
claudeReq.System for each system/developer message (msg.Role == "system" ||
msg.Role == "developer") which loses earlier messages; change the logic in that
branch to append (not assign) the extracted text to claudeReq.System, joining
multiple messages with a newline separator. For both content types (string and
[]interface{} parsed into systemText) append to claudeReq.System using a
conditional that inserts "\n" only when claudeReq.System is non-empty, and
ensure you preserve existing trimming/concatenation behavior so multi-part
[]interface{} content still aggregates into a single appended string.

In `@internal/converter/openai_to_claude_response.go`:
- Around line 27-29: The current code silently ignores additional entries in
resp.Choices by only using resp.Choices[0]; update the logic in this converter
(the code handling resp.Choices and variable choice) to explicitly reject or
error when len(resp.Choices) > 1 instead of dropping them silently: check if
len(resp.Choices) > 1 and return a clear error (or an explicit refusal response)
indicating multiple choices were returned, otherwise continue processing
resp.Choices[0] as before; make sure the error message references that multiple
choices were received so callers can handle or log the situation.

In `@internal/converter/openai_to_gemini_request.go`:
- Around line 214-219: The Gemini FunctionResponse being appended to toolParts
lacks the ID field required for v1internal compatibility; update the
construction of GeminiFunctionResponse inside the toolParts append so it sets
ID: tc.ID (which is available and non-empty) alongside Name and Response (i.e.,
ensure GeminiFunctionResponse{ID: tc.ID, Name: name, Response:
map[string]string{"result": resp}}) so the OpenAI-to-Gemini converter (in the
block that builds toolParts/GeminiPart) matches the Claude converter behavior.
🧹 Nitpick comments (5)
internal/converter/claude_to_gemini_stream.go (1)

15-18: JSON 反序列化错误被静默忽略。

json.Unmarshal 失败时,代码仅 continue 跳过该事件,没有任何日志记录或错误信息。这可能会隐藏潜在的数据格式问题,使调试变得困难。

建议至少添加日志记录以便排查问题。

internal/converter/claude_to_gemini_request.go (2)

78-84: TODO 注释表明功能不完整。

第 80 行存在 TODO 注释 // TODO: integrate with signature cacheglobalSig 被硬编码为空字符串。这可能导致在涉及函数调用时,签名验证逻辑无法正确工作。

是否需要我帮助追踪此 TODO 或创建一个 issue 来跟踪此待办事项?


299-306: funcName == "" 检查是死代码。

在第 268-270 行已经检查了 toolUseID == ""continue,因此到达第 300 行时 toolUseID 必定非空。funcName 被初始化为 toolUseID(第 300 行),即使 toolIDToName[toolUseID] 返回空值,funcName 仍保持为非空的 toolUseID。因此第 304-306 行的检查永远不会为真。

♻️ 建议移除死代码
 				funcName := toolUseID
 				if name, ok := toolIDToName[toolUseID]; ok {
 					funcName = name
 				}
-				if funcName == "" {
-					continue
-				}
internal/converter/openai_to_gemini_request.go (2)

137-145: 建议添加未知角色的默认处理。

当前 switch 缺少 default 分支,未知角色(如旧版 OpenAI 的 "function")会导致 geminiContent.Role 为空字符串,可能引发 Gemini API 错误。

♻️ 建议添加 default 分支
 		switch msg.Role {
 		case "user":
 			geminiContent.Role = "user"
 		case "assistant":
 			geminiContent.Role = "model"
 		case "system", "developer":
 			geminiContent.Role = "user"
+		default:
+			geminiContent.Role = "user"
 		}

182-194: 过往 JSON 解析问题已修复,但建议增加空名称校验。

json.Unmarshal 错误处理已正确添加。然而,根据 commit message 中 "skip tool blocks with empty name/id" 的描述,此处应与下方 FunctionResponse 逻辑(lines 200-209)保持一致,跳过 tc.Function.Nametc.ID 为空的 tool call。

♻️ 建议增加空名称/ID 校验
 		for _, tc := range msg.ToolCalls {
+			if tc.Function.Name == "" || tc.ID == "" {
+				continue
+			}
 			var args map[string]interface{}
 			if err := json.Unmarshal([]byte(tc.Function.Arguments), &args); err != nil {
 				return nil, err
 			}
📜 Review details

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 5d39bc1 and c378448.

📒 Files selected for processing (18)
  • internal/converter/claude_to_gemini_request.go
  • internal/converter/claude_to_gemini_stream.go
  • internal/converter/claude_to_openai_stream.go
  • internal/converter/codex_instructions/prompt.md-001-31d0d7a305305ad557035a2edcab60b6be5018d8
  • internal/converter/codex_instructions/prompt.md-002-6ce0a5875bbde55a00df054e7f0bceba681cf44d
  • internal/converter/codex_instructions/prompt.md-003-a6139aa0035d19d794a3669d6196f9f32a8c8352
  • internal/converter/codex_instructions/prompt.md-004-063083af157dcf57703462c07789c54695861dff
  • internal/converter/codex_instructions/prompt.md-005-d31e149cb1b4439f47393115d7a85b3c8ab8c90d
  • internal/converter/codex_instructions/prompt.md-006-81b148bda271615b37f7e04b3135e9d552df8111
  • internal/converter/codex_instructions/prompt.md-007-90d892f4fd5ffaf35b3dacabacdd260d76039581
  • internal/converter/codex_instructions/prompt.md-008-30ee24521b79cdebc8bae084385550d86db7142a
  • internal/converter/coverage_gemini_helpers_test.go
  • internal/converter/gemini_to_claude_request.go
  • internal/converter/gemini_to_codex.go
  • internal/converter/openai_to_claude_request.go
  • internal/converter/openai_to_claude_response.go
  • internal/converter/openai_to_gemini_request.go
  • internal/converter/openai_to_gemini_response.go
✅ Files skipped from review due to trivial changes (1)
  • internal/converter/codex_instructions/prompt.md-005-d31e149cb1b4439f47393115d7a85b3c8ab8c90d
🚧 Files skipped from review as they are similar to previous changes (7)
  • internal/converter/codex_instructions/prompt.md-001-31d0d7a305305ad557035a2edcab60b6be5018d8
  • internal/converter/claude_to_openai_stream.go
  • internal/converter/codex_instructions/prompt.md-004-063083af157dcf57703462c07789c54695861dff
  • internal/converter/openai_to_gemini_response.go
  • internal/converter/codex_instructions/prompt.md-002-6ce0a5875bbde55a00df054e7f0bceba681cf44d
  • internal/converter/gemini_to_claude_request.go
  • internal/converter/coverage_gemini_helpers_test.go
🧰 Additional context used
🧬 Code graph analysis (4)
internal/converter/claude_to_gemini_stream.go (4)
internal/converter/registry.go (2)
  • TransformState (11-19)
  • Usage (30-35)
internal/converter/sse.go (2)
  • ParseSSE (15-61)
  • FormatSSE (83-106)
internal/converter/types_claude.go (1)
  • ClaudeStreamEvent (105-112)
internal/converter/types_gemini.go (2)
  • GeminiCandidate (103-108)
  • GeminiContent (14-17)
internal/converter/gemini_to_codex.go (3)
internal/converter/codex_instructions.go (3)
  • ExtractCodexUserAgent (54-66)
  • CodexInstructionsForModel (141-149)
  • GetCodexInstructionsEnabled (29-34)
internal/adapter/provider/antigravity/transform_request.go (1)
  • ThinkingConfig (144-147)
internal/converter/types_codex.go (2)
  • CodexReasoning (41-44)
  • CodexTool (34-39)
internal/converter/openai_to_claude_response.go (3)
internal/converter/types_openai.go (1)
  • OpenAIResponse (79-87)
internal/converter/types_claude.go (3)
  • ClaudeResponse (86-95)
  • ClaudeUsage (97-102)
  • ClaudeContentBlock (37-55)
internal/converter/registry.go (1)
  • Usage (30-35)
internal/converter/openai_to_gemini_request.go (4)
internal/converter/register.go (1)
  • RegisterConverter (12-25)
internal/domain/model.go (2)
  • ClientTypeOpenAI (12-12)
  • ClientTypeGemini (11-11)
internal/converter/types_openai.go (1)
  • OpenAIRequest (5-25)
internal/converter/types_gemini.go (8)
  • GeminiRequest (5-12)
  • GeminiGenerationConfig (45-57)
  • GeminiImageConfig (65-68)
  • GeminiThinkingConfig (59-63)
  • GeminiContent (14-17)
  • GeminiFunctionResponse (39-43)
  • GeminiFunctionDecl (81-86)
  • GeminiTool (75-79)
🔇 Additional comments (17)
internal/converter/openai_to_claude_response.go (6)

10-14: JSON 反序列化的错误处理清晰可靠。


16-25: ClaudeResponse 基础映射简洁明确。


31-36: reasoning_content 转 thinking 处理合理。


39-59: 内容块转换逻辑清晰、可读性好。


62-76: tool_calls 参数解析与转换已覆盖错误返回。


78-86: finish_reason → stop_reason 映射清楚直观。

internal/converter/claude_to_gemini_request.go (2)

11-13: LGTM!

转换器注册和 Transform 方法入口点实现正确,JSON 解析错误被正确传播。

Also applies to: 17-21


167-175: 角色映射concern不适用于Claude API。

Claude API 的消息(ClaudeMessage)只支持 "user" 和 "assistant" 角色。系统消息通过请求级别的 System 字段处理,而非通过消息角色处理。因此,代码不会接收到 "developer" 或 "system" 角色值。

该concern可能适用于 OpenAI→Gemini 或 Codex→Gemini 转换器(这些API确实支持这些角色),但对Claude转换器不适用。当前代码是正确的。

Likely an incorrect or invalid review comment.

internal/converter/openai_to_gemini_request.go (4)

1-14: LGTM!

导入和转换器注册逻辑清晰,符合项目中 RegisterConverter 的使用模式。


72-87: 过往问题已修复。

reasoning_effort=none 的处理现在正确:设置 IncludeThoughts=false 并留空 ThinkingLevel,利用 omitempty 从请求中排除无效值。


236-253: LGTM!

工具转换逻辑正确,使用 ParametersJsonSchema 符合新版 Gemini API 规范,空参数的默认值处理也很合理。


255-260: LGTM!

ToolConfig 解析和最终 JSON 序列化逻辑清晰。

internal/converter/openai_to_claude_request.go (5)

1-15: 包结构与注册位置清晰。

组织方式直观,便于定位转换器入口。


16-32: 基础映射与 MaxTokens 回退清楚。

解析失败即返回、回退逻辑明确。


55-78: tool 消息与 thinking 块处理清楚。

流程明确,结构清晰。


114-120: 单文本扁平化逻辑简洁。

单一文本时降级为字符串,其他场景保留 block 数组,输出明确。


123-145: 工具与 stop 字段映射清楚。

转换与最终 marshal 返回清晰直接。

✏️ Tip: You can disable this entire section by setting review_details to false in your review settings.

Comment on lines +20 to +33
switch claudeEvent.Type {
case "content_block_delta":
if claudeEvent.Delta != nil && claudeEvent.Delta.Type == "text_delta" {
geminiChunk := GeminiStreamChunk{
Candidates: []GeminiCandidate{{
Content: GeminiContent{
Role: "model",
Parts: []GeminiPart{{Text: claudeEvent.Delta.Text}},
},
Index: 0,
}},
}
output = append(output, FormatSSE("", geminiChunk)...)
}
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

🧩 Analysis chain

🏁 Script executed:

rg -n "thinking_delta" --type=go

Repository: awsl-project/maxx

Length of output: 2526


🏁 Script executed:

# Find the test file for claude_to_gemini_stream.go
fd -e go | grep -i test | grep -i claude | grep -i gemini

Repository: awsl-project/maxx

Length of output: 473


🏁 Script executed:

# Check the full structure of the converter directory
ls -la internal/converter/

Repository: awsl-project/maxx

Length of output: 6231


🏁 Script executed:

# Get the complete content of the file in review to understand full context
cat -n internal/converter/claude_to_gemini_stream.go

Repository: awsl-project/maxx

Length of output: 2164


🏁 Script executed:

# Check the test file for claude_to_gemini_stream to see what's tested
cat -n internal/converter/claude_to_gemini_stream_test.go

Repository: awsl-project/maxx

Length of output: 3195


🏁 Script executed:

# Check the test file for claude_to_openai_stream to see how thinking_delta is handled there
cat -n internal/converter/claude_to_openai_stream.go | head -100

Repository: awsl-project/maxx

Length of output: 3696


🏁 Script executed:

# Check what ClaudeStreamDelta contains
cat -n internal/converter/types_claude.go | grep -A 10 "ClaudeStreamDelta"

Repository: awsl-project/maxx

Length of output: 597


缺少对 thinking_delta 事件的处理。

当前实现在 content_block_delta 中仅处理 text_delta 类型(第22行),thinking_delta 事件被静默丢弃。测试文件 claude_to_gemini_stream_test.goTestClaudeToGeminiResponse_StreamThinking 函数(第77-97行)期望处理 thinking_delta 事件。同时,claude_to_openai_stream.go 的实现(第69-81行)明确处理了 thinking_delta 类型,应用相同逻辑进行转换。

🤖 Prompt for AI Agents
In `@internal/converter/claude_to_gemini_stream.go` around lines 20 - 33, The
switch handling for claudeEvent.Type currently processes only
"content_block_delta" with a "text_delta" (claudeEvent.Delta.Type) and drops
"thinking_delta"; update the same branch in claude_to_gemini_stream.go to detect
when claudeEvent.Delta.Type == "thinking_delta" and emit a GeminiStreamChunk
identical in shape to the text case (use GeminiStreamChunk, GeminiCandidate,
GeminiContent with Role:"model" and Parts containing an empty or placeholder
GeminiPart as done for text), then append the result via FormatSSE just like the
existing logic so TestClaudeToGeminiResponse_StreamThinking passes and behavior
matches claude_to_openai_stream.go handling of "thinking_delta".

Comment on lines +35 to +48
case "message_delta":
if claudeEvent.Usage != nil {
if state.Usage == nil {
state.Usage = &Usage{}
}
state.Usage.OutputTokens = claudeEvent.Usage.OutputTokens
}

case "message_stop":
inputTokens, outputTokens := 0, 0
if state.Usage != nil {
inputTokens = state.Usage.InputTokens
outputTokens = state.Usage.OutputTokens
}
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

InputTokens 从未被设置,但在 message_stop 中被读取。

message_delta 事件中只设置了 state.Usage.OutputTokens(第 40 行),但 InputTokens 从未在此文件中被赋值。这意味着 message_stop 中的 PromptTokenCount 将始终为 0,除非 state.Usage.InputTokens 在外部被预先设置。

Claude SSE 流中的 message_start 事件通常包含 input_tokens。考虑添加对 message_start 事件的处理:

🔧 建议添加 message_start 处理
 		switch claudeEvent.Type {
+		case "message_start":
+			if claudeEvent.Message != nil && claudeEvent.Message.Usage != nil {
+				if state.Usage == nil {
+					state.Usage = &Usage{}
+				}
+				state.Usage.InputTokens = claudeEvent.Message.Usage.InputTokens
+			}
+
 		case "content_block_delta":
🤖 Prompt for AI Agents
In `@internal/converter/claude_to_gemini_stream.go` around lines 35 - 48, The code
updates state.Usage.OutputTokens in the "message_delta" case but never sets
state.Usage.InputTokens, so PromptTokenCount stays zero; add handling for the
"message_start" event to initialize state.Usage (if nil) and set
state.Usage.InputTokens from the incoming claudeEvent usage payload (e.g.,
claudeEvent.Usage.InputTokens or claudeEvent.InputTokens as appropriate),
mirroring the pattern used in "message_delta" to ensure state.Usage.InputTokens
is populated before "message_stop" reads it.

Comment on lines +34 to +53
// Convert messages
for _, msg := range req.Messages {
if msg.Role == "system" || msg.Role == "developer" {
// Extract system message
switch content := msg.Content.(type) {
case string:
claudeReq.System = content
case []interface{}:
var systemText string
for _, part := range content {
if m, ok := part.(map[string]interface{}); ok {
if text, ok := m["text"].(string); ok {
systemText += text
}
}
}
claudeReq.System = systemText
}
continue
}
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

🧩 Analysis chain

🌐 Web query:

OpenAI Chat Completions API multiple system messages role

💡 Result:

Yes—you can include multiple system messages in a single Chat Completions request: the API accepts messages as an ordered list of message objects, and system is one of the allowed roles. Messages are processed in the order they appear. [1], [2]

Practical implications:

  • If you send multiple system messages, treat them as cumulative instructions; if they conflict, the model will try to follow them in context order (so later ones often effectively “override” earlier ones, but it’s best not to rely on conflicts). [1]
  • For o1 models and newer, OpenAI recommends using developer messages for top-level instructions; in the Chat Completions schema, developer messages “replace the previous system messages” for those models. [2]

Best practice: keep one consolidated top-level instruction (one system/developer message) unless you have a clear reason to split it. [1]

Sources:
[1] OpenAI Help Center — “Moving from Completions to Chat Completions…” https://help.openai.com/en/articles/7042661-moving-from-completions-to-chat-completions-in-the-openai-api
[2] OpenAI API Reference — Chat Completions “messages” schema (system/developer roles) https://platform.openai.com/docs/api-reference/chat/create-chat-completion


🌐 Web query:

Claude API system prompt concatenation multiple instructions best practices

💡 Result:

How Claude “concatenates” system instructions (and how to structure them well)

  • There is one top-level system field (not a "system" message role). In the Messages API, system can be either a single string or an array of text blocks; blocks are effectively concatenated in the order you send them, so keep a deterministic order. [2]
  • If you pass tools, Claude will prepend tool-use instructions and tool schemas, then insert your system prompt inside that constructed system prompt. So your “real” system instructions are combined with (and come after) tool formatting/tool definitions. Don’t fight those tool instructions; align with them. [3]

Best practices for multiple instruction sources

  1. Keep system for stable “who you are / how to behave”, and put the task in the user message. Anthropic explicitly recommends using system to set the role, with task-specific instructions in the user turn. [1]
  2. Merge many instruction sources into one coherent, non-conflicting policy. If you must include multiple sections, use explicit headings (or XML tags) and define precedence (e.g., “Safety > Tool rules > Output format > Style preferences”).
  3. Prefer a fixed ordering like:
    • Identity / role
    • Non-negotiable rules (safety, privacy, refusal behavior)
    • Tool-use expectations (when to call tools; how to present tool outputs)
    • Output format constraints (JSON, schema, markdown sections)
    • Style / tone preferences
  4. Avoid repeating near-duplicates across system + user. Repetition can create conflicts; instead, make system stable and put per-request constraints in the user message. [1]
  5. For caching and performance, keep the “prefix” stable. Prompt caching treats the prompt prefix in the order toolssystemmessages, so put reusable instructions early and keep them identical between calls when possible. [4]

Practical pattern (recommended)

  • Build a single “base system” string (role + global rules).
  • Append small, clearly labeled optional sections only when truly necessary (still inside system, but as additional blocks or appended text).
  • Put everything request-specific (inputs, edge cases, output length for this run) in the user message. [1][2]

多条 system/developer 消息会被覆盖丢失。

第 36-51 行直接赋值 claudeReq.System,若请求包含多条 system/developer 消息,前面的指令会被后续消息覆盖。虽然 OpenAI 允许多条系统消息,但 Claude 仅有一个 system 字段,应将多条消息累加并用换行符分隔以保持完整信息。

建议改法(累加并分隔)
-			case string:
-				claudeReq.System = content
+			case string:
+				if strings.TrimSpace(content) != "" {
+					if claudeReq.System != "" {
+						claudeReq.System += "\n"
+					}
+					claudeReq.System += content
+				}
 			case []interface{}:
 				var systemText string
 				for _, part := range content {
 					if m, ok := part.(map[string]interface{}); ok {
 						if text, ok := m["text"].(string); ok {
 							systemText += text
 						}
 					}
 				}
-				claudeReq.System = systemText
+				if strings.TrimSpace(systemText) != "" {
+					if claudeReq.System != "" {
+						claudeReq.System += "\n"
+					}
+					claudeReq.System += systemText
+				}
 			}
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
// Convert messages
for _, msg := range req.Messages {
if msg.Role == "system" || msg.Role == "developer" {
// Extract system message
switch content := msg.Content.(type) {
case string:
claudeReq.System = content
case []interface{}:
var systemText string
for _, part := range content {
if m, ok := part.(map[string]interface{}); ok {
if text, ok := m["text"].(string); ok {
systemText += text
}
}
}
claudeReq.System = systemText
}
continue
}
// Convert messages
for _, msg := range req.Messages {
if msg.Role == "system" || msg.Role == "developer" {
// Extract system message
switch content := msg.Content.(type) {
case string:
if strings.TrimSpace(content) != "" {
if claudeReq.System != "" {
claudeReq.System += "\n"
}
claudeReq.System += content
}
case []interface{}:
var systemText string
for _, part := range content {
if m, ok := part.(map[string]interface{}); ok {
if text, ok := m["text"].(string); ok {
systemText += text
}
}
}
if strings.TrimSpace(systemText) != "" {
if claudeReq.System != "" {
claudeReq.System += "\n"
}
claudeReq.System += systemText
}
}
continue
}
🤖 Prompt for AI Agents
In `@internal/converter/openai_to_claude_request.go` around lines 34 - 53, The
loop over req.Messages currently overwrites claudeReq.System for each
system/developer message (msg.Role == "system" || msg.Role == "developer") which
loses earlier messages; change the logic in that branch to append (not assign)
the extracted text to claudeReq.System, joining multiple messages with a newline
separator. For both content types (string and []interface{} parsed into
systemText) append to claudeReq.System using a conditional that inserts "\n"
only when claudeReq.System is non-empty, and ensure you preserve existing
trimming/concatenation behavior so multi-part []interface{} content still
aggregates into a single appended string.

Comment on lines +79 to +99
// Convert content
switch content := msg.Content.(type) {
case string:
if content != "" {
blocks = append(blocks, ClaudeContentBlock{Type: "text", Text: content})
}
case []interface{}:
for _, part := range content {
if m, ok := part.(map[string]interface{}); ok {
partType, _ := m["type"].(string)
switch partType {
case "text":
text, _ := m["text"].(string)
if text != "" {
blocks = append(blocks, ClaudeContentBlock{Type: "text", Text: text})
}
}
}
}
}

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

🧩 Analysis chain

🌐 Web query:

OpenAI Chat Completions API message content types format specification

💡 Result:

Chat Completions messages[].content content-types (format spec)

In POST /v1/chat/completions, each item in messages is an object with at least:

  • role: "developer" | "system" | "user" | "assistant" | "tool" (and legacy "function")
  • content: either a string or an array of “content parts” (typed objects) [1].

1) content as a string

{ "role": "user", "content": "Hello!" }

(Allowed wherever content is supported.) [1]

2) content as an array of content parts

Each part is an object with a required type, plus fields determined by that type [1].

text part

{ "type": "text", "text": "Hello!" }

[1]

image_url part (user input)

{
  "type": "image_url",
  "image_url": { "url": "https://example.com/image.png", "detail": "auto" }
}
  • detail is optional (defaults to "auto") [1]

input_audio part (user input)

{
  "type": "input_audio",
  "input_audio": { "data": "<base64-audio>", "format": "wav" }
}
  • format currently supports "wav" and "mp3" [1]

file part (user input)

{
  "type": "file",
  "file": { "file_id": "file_...", "filename": "doc.pdf" }
}
  • file can be provided via file_id (uploaded file) or via base64 file_data (+ filename) [1]

refusal part (assistant output)

{ "type": "refusal", "refusal": "I can’t help with that." }

[1]


Role-based constraints (from the API reference)

  • developer / system / tool messages: content may be a string or parts array, but only type: "text" parts are supported. [1]
  • user messages: parts array can include text, image, audio, and file inputs (model-dependent). [1]
  • assistant messages: content can be a string or parts array containing one or more text parts, or exactly one refusal part. Also, content can be omitted if the assistant returns tool_calls / function_call. [1]

Source: OpenAI API Reference → Chat Completions: Create chat completion [1].

References

  1. OpenAI API Reference – Chat Completions: Create (messages and content schema) (platform.openai.com)

Citations:


🌐 Web query:

Claude API message content types text image supported formats

💡 Result:

In the Claude Messages API, messages[n].content can be either:

  • Plain string (treated as text), or
  • An array of content blocks, where each block has a type such as:
    • {"type":"text","text":"..."} (text block) [1]
    • {"type":"image","source":{...}} (image block) [2]

For image blocks, Claude supports these source.type values:

  • base64
  • url [1] [2]

Supported image formats / media types (source.media_type) are:

  • image/jpeg
  • image/png
  • image/gif
  • image/webp [1] [2] [3]

Sources: Anthropic Messages docs [1], Vision guide [2], Files docs (supported image MIME types) [3].


需要处理 OpenAI 的 image_url 部分,而非所有非 text 内容都应报错。

OpenAI Chat API 的 content parts 支持多种类型(text、image_url、input_audio、file 等),但当前代码仅处理 string 和 type=text 部分,其他内容被静默丢弃。其中 image_url 是需要重点处理的,因为 Claude API 支持 image 块(通过 base64 或 url)。input_audio 和 file 类型则不被 Claude 支持,这些可以报错或降级处理,但 image_url 应该被转换成 Claude 的 image 格式。

建议:

  • 添加 image_url 处理:将 OpenAI 的 image_url 转换为 Claude 的 image 块
  • 对 input_audio、file 等不支持的类型显式报错或日志警告
🤖 Prompt for AI Agents
In `@internal/converter/openai_to_claude_request.go` around lines 79 - 99, The
conversion of OpenAI message parts in the switch over msg.Content currently only
handles string and type="text" and silently drops other part types; update the
logic in the switch (the block that appends ClaudeContentBlock items) to
explicitly handle partType=="image_url" by creating a ClaudeContentBlock
representing an image (use the URL or fetch+base64 as appropriate for Claude's
expected image format), and for unsupported part types like "input_audio" and
"file" emit an explicit error or log warning instead of silently ignoring them;
update references to ClaudeContentBlock, msg.Content, and the inner partType
switch to implement this behavior.

Comment on lines +100 to +112
// Handle tool calls
for _, tc := range msg.ToolCalls {
var input interface{}
if err := json.Unmarshal([]byte(tc.Function.Arguments), &input); err != nil {
return nil, err
}
blocks = append(blocks, ClaudeContentBlock{
Type: "tool_use",
ID: tc.ID,
Name: tc.Function.Name,
Input: input,
})
}
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

🧩 Analysis chain

🌐 Web query:

Claude API tool_use content block ID Name field requirements

💡 Result:

In the Claude Messages API, an assistant tool_use content block must include:

  • id (required, string): an opaque unique identifier for that tool call, used to match the later tool_result.tool_use_id. The format/length may change over time, so treat it as an opaque string. [1][2]
  • name (required, string): the tool name being invoked; it must correspond to one of the tools you provided in the request. Tool definition names must match ^[a-zA-Z0-9_-]{1,64}$. [1]

Sources: Anthropic tool-use guide and API reference. [1][2]

References
[1] https://platform.claude.com/docs/en/agents-and-tools/tool-use/implement-tool-use
[2] https://platform.claude.com/docs/en/api/messages


🌐 Web query:

Anthropic Claude messages API tool_use structure specification

💡 Result:

tool_use content block (Claude Messages API)

When you include tools in a Messages API request, Claude may emit one or more assistant content blocks of:

{
  "type": "tool_use",
  "id": "toolu_...",
  "name": "your_tool_name",
  "input": { "any": "json_object_matching_your_input_schema" }
}
  • id is a unique identifier for that specific tool call (you use it to match results).
  • name matches one of the tool definitions you provided.
  • input is a JSON object that should conform to the tool’s input_schema. [1]

Corresponding tool_result block (what you send back)

You return execution results in a user message with content blocks like:

{
  "type": "tool_result",
  "tool_use_id": "toolu_...",
  "content": "result as a string OR an array of content blocks",
  "is_error": false
}

Notes:

  • Tool result blocks must immediately follow the assistant tool-use turn in the message history (no intervening messages). [1]
  • In the user message that contains tool results, tool_result blocks must come first in the content array (any explanatory text must come after all tool results). [1]

Tool definitions (what enables tool_use)

Tools are declared in the request’s top-level tools array:

{
  "name": "get_weather",
  "description": "...",
  "input_schema": { "type": "object", "properties": { ... }, "required": [ ... ] }
}

Optionally, you can add strict: true to enforce guaranteed schema-conformant tool inputs. [2]

Related protocol fields

  • For client-tool calling turns, the response stop_reason will be "tool_use". [1]

Recent behavior change worth knowing

  • You can use tool_choice: "none" to prevent tool calling, and you’re no longer required to provide tools just because tool blocks appear in the conversation. [3]

Sources: [1] https://platform.claude.com/docs/en/agents-and-tools/tool-use/implement-tool-use, [2] https://platform.claude.com/docs/en/agents-and-tools/tool-use/overview, [3] https://platform.claude.com/docs/en/release-notes/overview


tool_use 的 ID/Name 字段为必填项,需要进行空值校验。

根据 Claude Messages API 官方文档,tool_use 内容块中的 idname 都是必填字段

  • id:用于匹配后续 tool_result.tool_use_id,缺失则无法关联工具结果
  • name:必须对应请求中提供的工具定义,缺失则请求会被拒绝

建议在追加前进行校验,若 tc.IDtc.Function.Name 为空,应返回错误而非跳过,以便及时发现上游数据问题。

✅ 建议改法(返回错误)
 		for _, tc := range msg.ToolCalls {
+			if tc.ID == "" || tc.Function.Name == "" {
+				return nil, errors.New("tool_use requires non-empty ID and Name")
+			}
 			var input interface{}
 			if err := json.Unmarshal([]byte(tc.Function.Arguments), &input); err != nil {
 				return nil, err
 			}
🤖 Prompt for AI Agents
In `@internal/converter/openai_to_claude_request.go` around lines 100 - 112, The
loop handling msg.ToolCalls constructs ClaudeContentBlock entries of Type
"tool_use" but doesn't validate the required fields; add explicit non-empty
checks for tc.ID and tc.Function.Name before appending and return an error if
either is empty so callers surface upstream data problems. Update the for loop
that iterates msg.ToolCalls to validate tc.ID and tc.Function.Name, and if
either is blank return a descriptive error (mentioning "tool_use id" or
"tool_use name") instead of silently proceeding; keep the json.Unmarshal of
tc.Function.Arguments and the subsequent creation of ClaudeContentBlock
unchanged aside from this validation.

Comment on lines +27 to +29
if len(resp.Choices) > 0 {
choice := resp.Choices[0]
if choice.Message != nil {
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

避免静默丢弃多余 choices。
当前仅处理第一个 choice;若上游返回 n>1,会导致无提示的数据丢失。建议显式报错或明确拒绝多 choices。

🔧 建议修改(显式拒绝多 choices)
 import (
 	"encoding/json"
+	"fmt"
 	"strings"
 )
@@
-	if len(resp.Choices) > 0 {
-		choice := resp.Choices[0]
+	if len(resp.Choices) > 1 {
+		return nil, fmt.Errorf("openai response contains %d choices; expected 1", len(resp.Choices))
+	}
+	if len(resp.Choices) == 1 {
+		choice := resp.Choices[0]
🤖 Prompt for AI Agents
In `@internal/converter/openai_to_claude_response.go` around lines 27 - 29, The
current code silently ignores additional entries in resp.Choices by only using
resp.Choices[0]; update the logic in this converter (the code handling
resp.Choices and variable choice) to explicitly reject or error when
len(resp.Choices) > 1 instead of dropping them silently: check if
len(resp.Choices) > 1 and return a clear error (or an explicit refusal response)
indicating multiple choices were returned, otherwise continue processing
resp.Choices[0] as before; make sure the error message references that multiple
choices were received so callers can handle or log the situation.

Comment on lines +214 to +219
toolParts = append(toolParts, GeminiPart{
FunctionResponse: &GeminiFunctionResponse{
Name: name,
Response: map[string]string{"result": resp},
},
})
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

🧩 Analysis chain

🏁 Script executed:

# First, let's examine the structure of GeminiFunctionResponse in types_gemini.go
fd types_gemini.go -type f

Repository: awsl-project/maxx

Length of output: 231


🏁 Script executed:

# Search for GeminiFunctionResponse definition
rg -A 10 "type GeminiFunctionResponse struct" --type go

Repository: awsl-project/maxx

Length of output: 951


🏁 Script executed:

# Examine the context around lines 214-219 to understand the tc variable
sed -n '200,230p' internal/converter/openai_to_gemini_request.go

Repository: awsl-project/maxx

Length of output: 726


🏁 Script executed:

# Search for other FunctionResponse constructions to see if ID is set elsewhere
rg -B 3 -A 3 "FunctionResponse:" --type go

Repository: awsl-project/maxx

Length of output: 14519


🏁 Script executed:

# Check for v1internal endpoint usage in the codebase
rg -n "v1internal|v1/models|generateContent" --type go

Repository: awsl-project/maxx

Length of output: 5798


应在 FunctionResponse 中设置 ID 字段以确保 v1internal 端点兼容性。

根据代码分析,tc.ID 在此处可用(第 207 行已验证 tc.ID 不为空),且应在构建 FunctionResponse 时设置。该项目的 Claude 转换器(claude_to_gemini_request.go)已明确为 v1internal 兼容性设置 ID 字段,OpenAI 转换器应保持一致。

建议添加 ID 字段
 				toolParts = append(toolParts, GeminiPart{
 					FunctionResponse: &GeminiFunctionResponse{
 						Name:     name,
 						Response: map[string]string{"result": resp},
+						ID:       tc.ID,
 					},
 				})
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
toolParts = append(toolParts, GeminiPart{
FunctionResponse: &GeminiFunctionResponse{
Name: name,
Response: map[string]string{"result": resp},
},
})
toolParts = append(toolParts, GeminiPart{
FunctionResponse: &GeminiFunctionResponse{
Name: name,
Response: map[string]string{"result": resp},
ID: tc.ID,
},
})
🤖 Prompt for AI Agents
In `@internal/converter/openai_to_gemini_request.go` around lines 214 - 219, The
Gemini FunctionResponse being appended to toolParts lacks the ID field required
for v1internal compatibility; update the construction of GeminiFunctionResponse
inside the toolParts append so it sets ID: tc.ID (which is available and
non-empty) alongside Name and Response (i.e., ensure GeminiFunctionResponse{ID:
tc.ID, Name: name, Response: map[string]string{"result": resp}}) so the
OpenAI-to-Gemini converter (in the block that builds toolParts/GeminiPart)
matches the Claude converter behavior.

…lify embed and API

Keep only the latest version of each prompt type with clean names
(default.md, codex.md, codex_max.md, gpt51.md, gpt52.md, gpt52_codex.md),
removing 26 obsolete versioned files and unused review_prompt.md.

Replace embed.FS directory walk with individual go:embed string vars,
eliminating runtime iteration and HasPrefix matching. Simplify
CodexInstructionsForModel signature by removing unused systemInstructions
parameter and bool return value.

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants