From 22d02031d904bf2e0d5fc5a1b41caf6dc0a76e6b Mon Sep 17 00:00:00 2001 From: roboomp Date: Wed, 5 Aug 2026 02:02:19 +0000 Subject: [PATCH] fix(web-search): kept GPT-5.6 hosted tools top-level Codex Responses-Lite moves web_search into additional_tools, which the hosted backend ignores. Keep the classic top-level tools contract for all dedicated Codex web searches and cover GPT-5.6 request shape. Fixes #7666 --- packages/coding-agent/CHANGELOG.md | 4 ++ .../src/web/search/providers/codex.ts | 29 ++--------- .../test/tools/web-search-codex.test.ts | 50 +++---------------- 3 files changed, 13 insertions(+), 70 deletions(-) diff --git a/packages/coding-agent/CHANGELOG.md b/packages/coding-agent/CHANGELOG.md index 5bcee1054..48cb691cf 100644 --- a/packages/coding-agent/CHANGELOG.md +++ b/packages/coding-agent/CHANGELOG.md @@ -2,6 +2,10 @@ ## [Unreleased] +### Fixed + +- Fixed Codex web search sending GPT-5.6 models a Responses-Lite request shape that the hosted `web_search` tool ignores. ([#7666](https://github.com/can1357/oh-my-pi/issues/7666)) + ## [17.2.9] - 2026-08-05 ### Breaking Changes diff --git a/packages/coding-agent/src/web/search/providers/codex.ts b/packages/coding-agent/src/web/search/providers/codex.ts index ca32d4bef..2d6971957 100644 --- a/packages/coding-agent/src/web/search/providers/codex.ts +++ b/packages/coding-agent/src/web/search/providers/codex.ts @@ -13,11 +13,7 @@ import { withAuth, withOAuthAccess, } from "@oh-my-pi/pi-ai"; -import { applyCodexResponsesLiteShape } from "@oh-my-pi/pi-ai/providers/openai-codex/request-transformer"; -import { - createOpenAICodexCompatibilityMetadata, - resolveCodexResponsesUrl, -} from "@oh-my-pi/pi-ai/providers/openai-codex-responses"; +import { resolveCodexResponsesUrl } from "@oh-my-pi/pi-ai/providers/openai-codex-responses"; import { getBundledModels } from "@oh-my-pi/pi-catalog/models"; import { CODEX_BASE_URL, @@ -447,7 +443,6 @@ async function callCodexSearch( systemPrompt?: string; searchContextSize?: "low" | "medium" | "high"; model: CodexModelCandidate; - sessionId?: string; fetch?: FetchImpl; transport: CodexSearchTransport; }, @@ -455,7 +450,6 @@ async function callCodexSearch( const headers = buildCodexHeaders(auth.accessToken, auth.accountId, options.transport.headers); const requestedModel = options.model.modelId; - const usesResponsesLite = options.model.catalogModel?.useResponsesLite === true; const body: Record = { model: requestedModel, @@ -477,21 +471,6 @@ async function callCodexSearch( tool_choice: { type: "web_search" }, instructions: options.systemPrompt ?? DEFAULT_INSTRUCTIONS, }; - if (usesResponsesLite) { - const metadata = createOpenAICodexCompatibilityMetadata({ - sessionId: options.sessionId, - requestKind: "turn", - startNewTurn: true, - }); - for (const name in metadata.headers) { - const value = metadata.headers[name]; - if (value !== undefined) headers.set(name, value); - } - headers.set(OPENAI_HEADERS.RESPONSES_LITE, "true"); - body.client_metadata = metadata.clientMetadata; - body.reasoning = { context: "all_turns" }; - applyCodexResponsesLiteShape(body); - } const fetchImpl = options.fetch ?? fetch; const response = await fetchImpl(options.transport.url, { @@ -519,9 +498,8 @@ async function callCodexSearch( let model = requestedModel; let requestId = ""; let usage: { inputTokens: number; outputTokens: number; totalTokens: number } | undefined; - // Evidence that the hosted web_search tool actually ran. Lite models get - // `tool_choice: "auto"` and may answer without searching (#6988); a search - // command must reject that rather than return a non-search completion. + // A search command must reject a completion that did not invoke the hosted + // tool rather than returning an answer from the model's own knowledge (#6988). let webSearchInvoked = false; for await (const rawEvent of readSseJson>(response.body, options.signal)) { @@ -651,7 +629,6 @@ async function runCodexSearchCandidates(options: { systemPrompt: options.params.systemPrompt, searchContextSize: "high", model: candidate, - sessionId: options.params.sessionId, fetch: options.params.fetch, transport: options.transport, }); diff --git a/packages/coding-agent/test/tools/web-search-codex.test.ts b/packages/coding-agent/test/tools/web-search-codex.test.ts index 379bad0a8..ee084ef8d 100644 --- a/packages/coding-agent/test/tools/web-search-codex.test.ts +++ b/packages/coding-agent/test/tools/web-search-codex.test.ts @@ -330,9 +330,7 @@ describe("searchCodex model selection", () => { expect(sentUserText()).toBe('bun runtime "exact phrase" site:bun.sh -site:reddit.com after:2024-01-01'); // Tool config stays untouched: the ChatGPT backend's filter support is // unverified, so no `filters` field is added to the web_search tool. - const input = capturedRequest?.body?.input as Array>; - const additionalTools = input.find(item => item.type === "additional_tools"); - expect(additionalTools?.tools).toEqual([{ type: "web_search", search_context_size: "high" }]); + expect(capturedRequest?.body?.tools).toEqual([{ type: "web_search", search_context_size: "high" }]); }); it("sends directive-free queries byte-identical", async () => { @@ -470,67 +468,31 @@ describe("searchCodex model selection", () => { expect(result.sources).toEqual([{ title: "Example Article", url: "https://example.com/article" }]); }); - it("encodes explicit gpt-5.6-sol as a Responses-Lite request", async () => { + it("keeps hosted web_search top-level for explicit Responses-Lite catalog models (#7666)", async () => { process.env.PI_CODEX_WEB_SEARCH_MODEL = "gpt-5.6-sol"; const result = await searchCodex(makeSearchParams("Sol web search", mockCodexFetch("gpt-5.6-sol"))); expect(capturedRequest).not.toBeNull(); const headers = new Headers(capturedRequest?.headers); - expect(headers.get("x-openai-internal-codex-responses-lite")).toBe("true"); - expect(headers.get("session-id")).toBeTruthy(); - expect(headers.get("thread-id")).toBeTruthy(); - expect(headers.get("x-codex-window-id")).toBeTruthy(); + expect(headers.get("x-openai-internal-codex-responses-lite")).toBeNull(); expect(capturedRequest?.body).toEqual( expect.objectContaining({ model: "gpt-5.6-sol", - tool_choice: "auto", - reasoning: { context: "all_turns" }, - parallel_tool_calls: false, + tools: [{ type: "web_search", search_context_size: "high" }], + tool_choice: { type: "web_search" }, + instructions: "Codex test system prompt", input: [ - { - type: "additional_tools", - role: "developer", - tools: [{ type: "web_search", search_context_size: "high" }], - }, - { - type: "message", - role: "developer", - content: [{ type: "input_text", text: "Codex test system prompt" }], - }, { type: "message", role: "user", content: [{ type: "input_text", text: "Sol web search" }], }, ], - client_metadata: expect.objectContaining({ - session_id: headers.get("session-id"), - thread_id: headers.get("thread-id"), - "x-codex-window-id": headers.get("x-codex-window-id"), - }), }), ); - expect(capturedRequest?.body?.tools).toBeUndefined(); - expect(capturedRequest?.body?.instructions).toBeUndefined(); expect(result.model).toBe("gpt-5.6-sol"); }); - it("never leaves a forced hosted tool_choice on a Responses-Lite request (#5771)", async () => { - process.env.PI_CODEX_WEB_SEARCH_MODEL = "gpt-5.6-sol"; - await searchCodex(makeSearchParams("forced choice guard", mockCodexFetch("gpt-5.6-sol"))); - - const body = capturedRequest?.body; - expect(body).not.toBeNull(); - // Lite moves tools into `additional_tools` and drops top-level `tools`; - // a forced hosted choice against absent top-level tools is rejected 400. - const additionalTools = (body?.input as Array>)?.[0]; - expect(additionalTools?.type).toBe("additional_tools"); - expect(additionalTools?.tools).toEqual([{ type: "web_search", search_context_size: "high" }]); - expect(body?.tools).toBeUndefined(); - expect(body?.tool_choice).toBe("auto"); - expect(body?.tool_choice).not.toEqual({ type: "web_search" }); - }); - it("does not retry default candidates when PI_CODEX_WEB_SEARCH_MODEL is explicitly unsupported", async () => { process.env.PI_CODEX_WEB_SEARCH_MODEL = "gpt-5.5"; let calls = 0;