Skip to content

fix: bridge OpenRouter Codex tools via chat completions#562

Open
awsl233777 wants to merge 2 commits into
mainfrom
fix/openrouter-responses-tools
Open

fix: bridge OpenRouter Codex tools via chat completions#562
awsl233777 wants to merge 2 commits into
mainfrom
fix/openrouter-responses-tools

Conversation

@awsl233777
Copy link
Copy Markdown
Collaborator

@awsl233777 awsl233777 commented May 15, 2026

Summary

Fixes #537 by routing custom OpenRouter-compatible Codex requests through OpenAI Chat Completions when the provider also supports OpenAI format.

This avoids sending Codex Responses API tool schemas directly to OpenRouter-compatible /responses endpoints, where non-openrouter:* tool types are rejected.

Changes

  • Detect custom OpenRouter-compatible Codex routes by OpenRouter base URL / client base URL / provider name.
  • Convert those requests from Codex Responses shape to OpenAI Chat Completions before dispatch.
  • Normalize Codex developer messages to Chat-compatible system messages during Codex → OpenAI conversion.
  • Preserve function tools while dropping Responses-only built-in tools such as web_search that cannot be represented as Chat Completions function tools.
  • Add regression tests for the bridge detection, /responses/v1/chat/completions path, role normalization, and tool filtering.

Validation

  • go test ./internal/converter ./internal/executor ./internal/adapter/provider/custom
  • go test ./internal/...
  • Real local smoke test with maxx /responses → telepub DeepSeek-V4-pro, including a function tool payload, returned HTTP 200 and maxx537-ok.
  • Real codexmate codex exec smoke test through local maxx using telepub DeepSeek-V4-pro; Codex CLI request included 44 tools including web_search, and returned maxx537-codexmate-ok.

No API keys are included in this PR.

Summary by CodeRabbit

  • Bug Fixes

    • 改进消息角色的标准化逻辑,统一角色映射以减少异常角色导致的问题
  • New Features

    • 自动桥接对 OpenRouter 兼容的自定义 Codex 提供商,改为通过 OpenAI Chat Completions 路径处理请求
    • 工具过滤:仅保留形状匹配的函数类工具以避免不兼容工具传递
  • Tests

    • 增加 OpenRouter 桥接判定与请求路径转换的测试覆盖
    • 增加工具转换及响应工具筛除的测试用例

Review Change Stack

@coderabbitai
Copy link
Copy Markdown

coderabbitai Bot commented May 15, 2026

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: 094ab6c8-2fe4-44e5-9ed5-c6c7ad77551b

📥 Commits

Reviewing files that changed from the base of the PR and between 8aac1b5 and 11e7922.

📒 Files selected for processing (3)
  • internal/converter/codex_openai_more_test.go
  • internal/converter/codex_to_openai.go
  • internal/executor/openrouter_bridge.go
🚧 Files skipped from review as they are similar to previous changes (3)
  • internal/converter/codex_openai_more_test.go
  • internal/converter/codex_to_openai.go
  • internal/executor/openrouter_bridge.go

📝 Walkthrough

Walkthrough

该 PR 添加了 OpenRouter 兼容判定并在 Executor.dispatch 中按需将自定义 Codex 请求桥接为 OpenAI Chat Completions:实现角色映射与工具过滤的转换器、OpenRouter 判定函数及其测试,并将转换流程集成到请求分发中(失败时回退原格式)。

变更内容

Codex 到 OpenAI 的转换和工具过滤

Layer / File(s) Summary
消息角色规范化和工具类型过滤
internal/converter/codex_to_openai.go, internal/converter/codex_openai_more_test.go
添加 codexMessageRoleToOpenAI 将 developer/system 归为 system,其它角色小写或默认 user;工具转换仅保留规范化后 type=="function"name 非空的工具。新增测试验证工具被映射为函数工具并校验消息角色顺序。

OpenRouter 桥接判定和兼容性检查

Layer / File(s) Summary
OpenRouter 兼容性判定逻辑
internal/executor/openrouter_bridge.go, internal/executor/openrouter_bridge_test.go
新增 shouldBridgeCustomCodexViaOpenAI,并提供 supportsClientTypeisOpenRouterCompatibleURLisOpenRouterCompatibleProviderName。按 Custom.BaseURL、Custom.ClientBaseURL 与 provider 名称检测 OpenRouter 兼容性。测试覆盖正向与多类反向场景,并验证 URI 转换行为。

请求分发流程集成

Layer / File(s) Summary
Dispatcher 中的桥接分支
internal/executor/middleware_dispatch.go
在 Executor.dispatch 中添加分支:若判定应桥接则将 clientType 置为 OpenAI、调用转换器生成 OpenAI 请求体并转换 URI;若转换失败则回退并禁用转换;保留原有 NeedConvert 路径作为 else if。

序列图

sequenceDiagram
  participant Client as Codex CLI
  participant Dispatcher as Executor.dispatch
  participant BridgeChecker as shouldBridgeCustomCodexViaOpenAI
  participant Converter as CodexToOpenAI
  participant URIHandler as ConvertRequestURI
  participant OpenAI as OpenAI Chat Completions
  Client->>Dispatcher: /api/openrouter/responses (Codex 请求)
  Dispatcher->>BridgeChecker: 检查 custom OpenRouter provider
  BridgeChecker-->>Dispatcher: true/false
  alt 需要桥接
    Dispatcher->>Converter: TransformRequest (角色+工具转换)
    alt 转换成功
      Converter-->>Dispatcher: OpenAI 请求体
      Dispatcher->>URIHandler: /responses -> /v1/chat/completions
      URIHandler-->>Dispatcher: /v1/chat/completions
      Dispatcher->>OpenAI: 转换后的 OpenAI 格式请求
    else 转换失败
      Dispatcher-->>Client: 回退原始请求流程或错误
    end
  else 无需桥接
    Dispatcher->>Converter: 走 NeedConvert 通用路径
  end
  OpenAI-->>Dispatcher: 响应
  Dispatcher-->>Client: 返回结果
Loading

审查工作量估计

🎯 4 (Complex) | ⏱️ ~45 minutes

建议审查人员

  • ymkiux
  • dreamhunter2333

诗歌

🐰 路上遇到 OpenRouter,桥梁我要搭首座,
消息换角色,工具只留函数名,
转换成功跳向 Chat,失败则退回原路行,
小小兔子鼓掌叫好,兼容之桥静静落座。

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 7.14% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title accurately describes the main change: routing custom OpenRouter-compatible Codex requests through OpenAI Chat Completions to avoid tool schema validation errors.
Linked Issues check ✅ Passed The PR fully addresses issue #537 by implementing OpenRouter detection, Codex-to-OpenAI request conversion, message role normalization, and tool filtering to ensure compatibility.
Out of Scope Changes check ✅ Passed All changes are directly related to bridging OpenRouter-compatible Codex requests through OpenAI Chat Completions, with no extraneous modifications detected.

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

✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch fix/openrouter-responses-tools

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
Copy Markdown

@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.

🧹 Nitpick comments (3)
internal/converter/codex_to_openai.go (1)

140-149: ⚡ Quick win

可以优化重复的字符串处理

函数在 141 行已经对 role 执行了 strings.ToLower(strings.TrimSpace(role)),但在 145 行又重复执行了相同的操作。建议直接返回 switch 语句匹配到的原始值(已经是标准化后的小写形式)。

♻️ 优化建议
 func codexMessageRoleToOpenAI(role string) string {
-	switch strings.ToLower(strings.TrimSpace(role)) {
+	normalized := strings.ToLower(strings.TrimSpace(role))
+	switch normalized {
 	case "developer", "system":
 		return "system"
 	case "assistant", "user", "tool", "function":
-		return strings.ToLower(strings.TrimSpace(role))
+		return normalized
 	default:
 		return "user"
 	}
 }
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@internal/converter/codex_to_openai.go` around lines 140 - 149, The function
codexMessageRoleToOpenAI calls strings.ToLower(strings.TrimSpace(role)) twice;
normalize role once into a local variable (e.g., normalized :=
strings.ToLower(strings.TrimSpace(role))) then use that variable in the switch
and return the matched value directly (for example return normalized for the
assistant/user/tool/function cases) to eliminate redundant string processing and
keep logic clear.
internal/executor/openrouter_bridge.go (1)

52-65: 💤 Low value

后备字符串检查可能存在误报风险

64 行的 strings.Contains(trimmed, "openrouter.ai") 可以匹配包含该字符串的任意 URL,例如 https://evil.com/path/openrouter.ai/fake 也会返回 true。虽然这种场景在实际配置中不太可能出现,但如果需要更严格的验证,建议移除此后备检查或增加额外的上下文验证(例如检查是否出现在域名部分)。

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@internal/executor/openrouter_bridge.go` around lines 52 - 65, The fallback
strings.Contains(trimmed, "openrouter.ai") in isOpenRouterCompatibleURL can
yield false positives for paths or query strings; instead, require a successful
url.Parse and validate the domain portion only (use parsed.Hostname(), strip
"www.", then check equality or hasSuffix ".openrouter.ai"); drop the broad
strings.Contains check and return false if parsing fails so only host-based
matches pass.
internal/converter/codex_openai_more_test.go (1)

62-64: ⚡ Quick win

断言条件可以更精确

当前断言 len(got.Messages) < 2 只验证至少有 2 条消息,但根据输入(1 条 developer、1 条 user)应该恰好有 2 条消息。建议使用相等性检查以提高测试的准确性。

💚 改进建议
-	if len(got.Messages) < 2 || got.Messages[0].Role != "system" || got.Messages[1].Role != "user" {
+	if len(got.Messages) != 2 || got.Messages[0].Role != "system" || got.Messages[1].Role != "user" {
 		t.Fatalf("unexpected converted roles: %#v", got.Messages)
 	}
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@internal/converter/codex_openai_more_test.go` around lines 62 - 64, The
assertion is too loose—replace the length check with an exact equality so the
test verifies there are exactly two converted messages; specifically update the
condition that inspects got.Messages (the current if using len(got.Messages) <
2) to assert len(got.Messages) == 2 and still verify the roles order
(got.Messages[0].Role == "system" and got.Messages[1].Role == "user"), and
adjust the t.Fatalf message accordingly to reflect the stricter expectation.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Nitpick comments:
In `@internal/converter/codex_openai_more_test.go`:
- Around line 62-64: The assertion is too loose—replace the length check with an
exact equality so the test verifies there are exactly two converted messages;
specifically update the condition that inspects got.Messages (the current if
using len(got.Messages) < 2) to assert len(got.Messages) == 2 and still verify
the roles order (got.Messages[0].Role == "system" and got.Messages[1].Role ==
"user"), and adjust the t.Fatalf message accordingly to reflect the stricter
expectation.

In `@internal/converter/codex_to_openai.go`:
- Around line 140-149: The function codexMessageRoleToOpenAI calls
strings.ToLower(strings.TrimSpace(role)) twice; normalize role once into a local
variable (e.g., normalized := strings.ToLower(strings.TrimSpace(role))) then use
that variable in the switch and return the matched value directly (for example
return normalized for the assistant/user/tool/function cases) to eliminate
redundant string processing and keep logic clear.

In `@internal/executor/openrouter_bridge.go`:
- Around line 52-65: The fallback strings.Contains(trimmed, "openrouter.ai") in
isOpenRouterCompatibleURL can yield false positives for paths or query strings;
instead, require a successful url.Parse and validate the domain portion only
(use parsed.Hostname(), strip "www.", then check equality or hasSuffix
".openrouter.ai"); drop the broad strings.Contains check and return false if
parsing fails so only host-based matches pass.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: d33701b1-ecdd-4ccd-84bf-f5902396905d

📥 Commits

Reviewing files that changed from the base of the PR and between dcb07aa and 8aac1b5.

📒 Files selected for processing (5)
  • internal/converter/codex_openai_more_test.go
  • internal/converter/codex_to_openai.go
  • internal/executor/middleware_dispatch.go
  • internal/executor/openrouter_bridge.go
  • internal/executor/openrouter_bridge_test.go
📜 Review details
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (3)
  • GitHub Check: Backend Checks
  • GitHub Check: e2e
  • GitHub Check: playwright
🔇 Additional comments (11)
internal/converter/codex_to_openai.go (2)

83-83: LGTM!


119-126: LGTM!

internal/executor/openrouter_bridge.go (3)

17-41: LGTM!


43-50: LGTM!


67-69: LGTM!

internal/executor/openrouter_bridge_test.go (5)

9-21: LGTM!


23-35: LGTM!


37-49: LGTM!


51-66: LGTM!


68-75: LGTM!

internal/executor/middleware_dispatch.go (1)

78-99: LGTM!

@awsl233777
Copy link
Copy Markdown
Collaborator Author

Follow-up for head 11e7922.

This round addresses the review nits from CodeRabbit:

  • normalized Codex message roles once in codexMessageRoleToOpenAI instead of repeating trim/lower work;
  • tightened OpenRouter-compatible URL detection to validate the parsed hostname only, removing broad strings.Contains(..., "openrouter.ai") matching against paths/query strings;
  • made the converted-message role test assert exactly two messages instead of len < 2.

Validation run locally:

  • go test ./internal/converter ./internal/executor
  • go test ./internal/...

Remaining known status from my side:

  • no unresolved local test failure observed;
  • this PR still needs GitHub branch-protection review approval before merge if reviewDecision remains REVIEW_REQUIRED.

@coderabbitai review

@coderabbitai
Copy link
Copy Markdown

coderabbitai Bot commented May 15, 2026

@awsl233777 Thanks for the follow-up and for addressing the previous review nits! I'll kick off a fresh review of 11e7922 now.

✅ Actions performed

Review triggered.

Note: CodeRabbit is an incremental review system and does not re-review already reviewed commits. This command is applicable only when automatic reviews are paused.

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.

Codex CLI tools are incompatible with OpenRouter Responses API

1 participant