From 29fd4f02484250ebaf6ec256f18a7f8a31b25dd6 Mon Sep 17 00:00:00 2001 From: Ingwannu Date: Wed, 12 Aug 2026 09:25:01 +0000 Subject: [PATCH] fix(codex): keep direct MCP tools visible for routed models --- src/codex/catalog/parsing.ts | 13 +++++++------ src/codex/catalog/sync.ts | 2 +- structure/03_catalog-and-subagents.md | 18 ++++++++++++++++++ tests/catalog-cursor-search.test.ts | 6 +++--- tests/codex-catalog.test.ts | 10 +++++----- tests/e2e-style/phase100-native-parity.test.ts | 2 +- 6 files changed, 35 insertions(+), 16 deletions(-) diff --git a/src/codex/catalog/parsing.ts b/src/codex/catalog/parsing.ts index 7d69b811d5..5b1797788e 100644 --- a/src/codex/catalog/parsing.ts +++ b/src/codex/catalog/parsing.ts @@ -393,17 +393,18 @@ export function normalizeRoutedCatalogEntry(entry: RawEntry, parallelToolCalls = // Per-model routed opt-ins can be added once provider metadata exposes this capability. delete entry.supports_reasoning_summaries; const isCursorEntry = typeof entry.slug === "string" && entry.slug.startsWith("cursor/"); - // Routed providers use opencodex sidecars and client-executed tool discovery. The sidecar - // runs through native gpt-5.4-mini, so image search is available and verbalized for text-only - // models. EXCEPT cursor: its runTurn transport bypasses the web-search plan entirely and - // rejects server search queries — advertising the tool would make models call into a void. + // `supports_search_tool` selects Codex's deferred tool-discovery surface; it is not the hosted + // web-search capability. OpenCodex can round-trip tool_search when a client sends it, but routed + // providers have no provider/model proof that Codex App plugins work through that deferred + // surface. Advertising it unconditionally hides the App's compatible direct MCP tools (#1522), + // so routed rows fail closed to direct discovery. The sidecar-backed hosted web-search metadata + // remains advertised independently for non-Cursor routes. if (isCursorEntry) { delete entry.web_search_tool_type; - entry.supports_search_tool = false; } else { entry.web_search_tool_type = "text_and_image"; - entry.supports_search_tool = true; } + entry.supports_search_tool = false; // Cursor's transport already serializes overlapping tool calls into atomic Responses tool events. // Advertising parallel calls lets Codex send the same native capability bit it sends for OpenAI. // Opt-in providers (OcxProviderConfig.parallelToolCalls, e.g. xAI) advertise it too: the diff --git a/src/codex/catalog/sync.ts b/src/codex/catalog/sync.ts index 9f7a169217..90a5e207d8 100644 --- a/src/codex/catalog/sync.ts +++ b/src/codex/catalog/sync.ts @@ -313,7 +313,7 @@ export function deriveEntry( slug, display_name: routedDisplayName(slug), description: desc, shell_type: "shell_command", visibility: "list", supported_in_api: true, priority, base_instructions: "You are a helpful coding assistant.", - ...(isRouted ? { web_search_tool_type: "text_and_image", supports_search_tool: true } : {}), + ...(isRouted ? { web_search_tool_type: "text_and_image", supports_search_tool: false } : {}), }; if (isRouted) { applyRoutedCodexToolMode(entry); diff --git a/structure/03_catalog-and-subagents.md b/structure/03_catalog-and-subagents.md index d35f292728..1d048b58de 100644 --- a/structure/03_catalog-and-subagents.md +++ b/structure/03_catalog-and-subagents.md @@ -140,6 +140,24 @@ The `multi_agent_v2` feature flag and the logical maximum thread count are separ `multiAgentMode` (`src/codex/features.ts`): the mode decides which surface Codex advertises, while the flag and thread count decide what the native runtime allows. +## Routed tool discovery and hosted search + +Routed catalog rows advertise `supports_search_tool: false` by default. That field selects Codex's +deferred tool-discovery surface; it does not describe the hosted web-search sidecar. OpenCodex still +round-trips an explicit `tool_search` request, but it does not claim that every routed provider/model +can discover Codex App plugins through that surface. The conservative catalog value keeps direct MCP +tools visible in Codex App. Non-Cursor routed rows independently keep +`web_search_tool_type: "text_and_image"` for the OpenCodex search sidecar; Cursor advertises neither +because its transport bypasses that sidecar. + +[Decision Log] +- 목적과 의도: routed models must not hide direct Codex App plugin tools behind an unverified deferred discovery capability. +- 기존 구현 및 제약 조건: every non-Cursor row advertised `supports_search_tool: true`; the parser and bridge can still relay explicit `tool_search` calls. +- 검토한 주요 대안: keep the blanket flag, disable both deferred discovery and hosted search, or add a future evidence-backed provider/model opt-in. +- 선택한 방식: default routed deferred discovery to false while preserving the independent non-Cursor hosted-search metadata. +- 다른 대안 대신 이 방식을 선택한 이유: it fixes plugin availability without removing the existing web-search sidecar or deleting runtime protocol support. +- 장점, 단점 및 영향: direct MCP tools remain available; a routed model cannot use Codex's deferred discovery solely from generated catalog metadata until a verified opt-in exists. + ## Ultra reasoning level Ultra is always advertised in the catalog regardless of the `multi_agent_v2` toggle. The v2 toggle diff --git a/tests/catalog-cursor-search.test.ts b/tests/catalog-cursor-search.test.ts index cafef52752..c3d554f2f8 100644 --- a/tests/catalog-cursor-search.test.ts +++ b/tests/catalog-cursor-search.test.ts @@ -1,7 +1,7 @@ import { describe, expect, test } from "bun:test"; import { normalizeRoutedCatalogEntry } from "../src/codex/catalog"; -describe("cursor catalog search advertising", () => { +describe("routed catalog search advertising", () => { test("cursor entries do not advertise the hosted search tool (runTurn bypasses the sidecar)", () => { const entry = normalizeRoutedCatalogEntry({ slug: "cursor/auto" } as never) as Record; expect(entry.supports_search_tool).toBe(false); @@ -9,9 +9,9 @@ describe("cursor catalog search advertising", () => { expect(entry.supports_parallel_tool_calls).toBe(true); }); - test("non-cursor routed entries keep the sidecar-backed search advertisement", () => { + test("non-cursor routed entries keep hosted web search without opting into deferred discovery", () => { const entry = normalizeRoutedCatalogEntry({ slug: "opencode-go/glm-5.2" } as never) as Record; - expect(entry.supports_search_tool).toBe(true); + expect(entry.supports_search_tool).toBe(false); expect(entry.web_search_tool_type).toBe("text_and_image"); expect(entry.supports_parallel_tool_calls).toBe(false); }); diff --git a/tests/codex-catalog.test.ts b/tests/codex-catalog.test.ts index e57b1481dc..d85bdaf304 100644 --- a/tests/codex-catalog.test.ts +++ b/tests/codex-catalog.test.ts @@ -325,7 +325,7 @@ describe("combo catalog capability intersection", () => { expect(row).not.toHaveProperty("model_messages"); expect(row.tool_mode).toBe("code_mode_only"); expect(row.web_search_tool_type).toBe("text_and_image"); - expect(row.supports_search_tool).toBe(true); + expect(row.supports_search_tool).toBe(false); } }); @@ -2246,7 +2246,7 @@ describe("Codex catalog routed normalization", () => { expect(entry).not.toHaveProperty("service_tiers"); expect(entry).not.toHaveProperty("default_service_tier"); expect(entry.web_search_tool_type).toBe("text_and_image"); - expect(entry.supports_search_tool).toBe(true); + expect(entry.supports_search_tool).toBe(false); }); test("buildCatalogEntries strips routed entries cloned from native templates", () => { @@ -2268,7 +2268,7 @@ describe("Codex catalog routed normalization", () => { expect(routed).not.toHaveProperty("service_tiers"); expect(routed).not.toHaveProperty("default_service_tier"); expect(routed?.web_search_tool_type).toBe("text_and_image"); - expect(routed?.supports_search_tool).toBe(true); + expect(routed?.supports_search_tool).toBe(false); expect(routed?.supports_reasoning_summaries).toBe(false); expect(routed?.base_instructions).not.toBe(nativeTemplate().base_instructions); expect(routed?.base_instructions).toContain("claude-sonnet-4-6"); @@ -2723,14 +2723,14 @@ describe("Codex catalog routed normalization", () => { expect(off.find(e => e.slug === "anthropic/claude-sonnet-4-6")).not.toHaveProperty("supports_websockets"); }); - test("fallback routed entries still receive explicit search metadata", () => { + test("fallback routed entries keep hosted search metadata but use direct MCP discovery", () => { const entries = buildCatalogEntries(null, [], [ { provider: "local", id: "qwen3-coder" }, ]); const routed = entries.find(e => e.slug === "local/qwen3-coder"); expect(routed?.web_search_tool_type).toBe("text_and_image"); - expect(routed?.supports_search_tool).toBe(true); + expect(routed?.supports_search_tool).toBe(false); }); test("liveModels false uses configured provider models without fetching", async () => { diff --git a/tests/e2e-style/phase100-native-parity.test.ts b/tests/e2e-style/phase100-native-parity.test.ts index fa60965738..97c80b434a 100644 --- a/tests/e2e-style/phase100-native-parity.test.ts +++ b/tests/e2e-style/phase100-native-parity.test.ts @@ -81,7 +81,7 @@ describe("Phase 100 Codex-native parity smoke", () => { const routed = catalog.find(entry => entry.slug === "opencode-go/deepseek-v4-pro"); expect(routed).toMatchObject({ web_search_tool_type: "text_and_image", - supports_search_tool: true, + supports_search_tool: false, context_window: 1_000_000, auto_compact_token_limit: 900_000, });