Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
13 changes: 7 additions & 6 deletions src/codex/catalog/parsing.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
2 changes: 1 addition & 1 deletion src/codex/catalog/sync.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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 } : {}),

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

🗄️ Data Integrity & Integration | 🟠 Major | ⚡ Quick win

Do not advertise hosted search on Cursor fallback rows.

If template === null and model describes a cursor/... route, this spread adds web_search_tool_type: "text_and_image". The normal path in src/codex/catalog/parsing.ts Lines 395 through 405 removes that field for Cursor entries. Cursor bypasses the hosted-search sidecar, so the fallback catalog can advertise an unavailable capability.

Reuse normalizeRoutedCatalogEntry in this fallback path, or condition web_search_tool_type on !slug.startsWith("cursor/"). Add a regression test for a Cursor fallback entry.

As per path instructions, this src/** change must preserve the shared provider capability contract. The supplied PR objective defines Cursor rows as sidecar-free.

🧰 Tools
🪛 ast-grep (0.45.1)

[warning] Importing child_process exposes a command-execution surface; ensure any command/argument built from input is validated, and prefer execFile/spawn with an argument array over exec.
Context: import { execFileSync } from "node:child_process";
Note: [CWE-78] Improper Neutralization of Special Elements used in an OS Command ('OS Command Injection').

(detect-child-process-typescript)

🤖 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 `@src/codex/catalog/sync.ts` at line 316, Update the fallback catalog-row
construction around the isRouted spread so Cursor routes do not receive
web_search_tool_type or supports_search_tool. Reuse normalizeRoutedCatalogEntry
if applicable, or gate the hosted-search fields on !slug.startsWith("cursor/"),
while preserving the shared provider capability contract. Add a regression test
covering a Cursor fallback entry.

Source: Path instructions

};
if (isRouted) {
applyRoutedCodexToolMode(entry);
Expand Down
18 changes: 18 additions & 0 deletions structure/03_catalog-and-subagents.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
6 changes: 3 additions & 3 deletions tests/catalog-cursor-search.test.ts
Original file line number Diff line number Diff line change
@@ -1,17 +1,17 @@
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<string, unknown>;
expect(entry.supports_search_tool).toBe(false);
expect(entry.web_search_tool_type).toBeUndefined();
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<string, unknown>;
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);
});
Expand Down
10 changes: 5 additions & 5 deletions tests/codex-catalog.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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);
}
});

Expand Down Expand Up @@ -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", () => {
Expand All @@ -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");
Expand Down Expand Up @@ -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 () => {
Expand Down
2 changes: 1 addition & 1 deletion tests/e2e-style/phase100-native-parity.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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,
});
Expand Down
Loading