From d5ea14a2d522b428ff5b13d011bdf9e376b41358 Mon Sep 17 00:00:00 2001 From: bitkyc08-arch Date: Fri, 21 Aug 2026 16:37:17 +0900 Subject: [PATCH] fix(router): backfill the xAI web-search capability into routed provider config Routed Grok turns on the Responses lane died with `400 Argument not supported: external_web_access` before inference. routedProviderConfig() backfills every other registry-only scalar (supportsServiceTier, preserveResponsesReasoningContent, fastWire) but not supportsOpenAiWebSearchToolFields. enrichProviderFromRegistry() does fill it, and the request path never calls that function -- so a saved xai row reached the Responses adapter with the flag undefined. The #2262 capability gate reads undefined as "unclassified upstream, keep the fields", so Codex's OpenAI-only web_search config went to the wire and xAI rejected the whole request. Live probe against the OAuth Grok endpoint (2026-08-21) isolates the cause: bare {type:"web_search"} returns 200, +external_web_access returns 400, and +search_context_size returns 400 -- individually, before inference. Verified end-to-end on a remote macOS host running this dev head: with the GUI Responses opt-in on and NO hand-written capability in config.json, a multi-step codex exec tool-use turn now completes over adapter "openai-responses" with status 200, where the same turn 400'd before. The existing tests could not catch this: they hand-build a provider with the flag already set, or call enrichProviderFromRegistry() directly, so both start downstream of the break. The new tests assert on routedProviderConfig() output and were driven red against the unfixed router. --- src/router.ts | 11 ++++ ...responses-routed-web-search-fields.test.ts | 50 +++++++++++++++++++ 2 files changed, 61 insertions(+) diff --git a/src/router.ts b/src/router.ts index fa2f72e831..35e34d75ca 100644 --- a/src/router.ts +++ b/src/router.ts @@ -355,6 +355,17 @@ export function routedProviderConfig(providerName: string, provider: OcxProvider ...(provider.supportsServiceTier === undefined && registryEntry.supportsServiceTier !== undefined ? { supportsServiceTier: registryEntry.supportsServiceTier } : {}), + // Registry-only web-search capability: without this backfill a saved provider row reaches + // the Responses adapter with the flag `undefined`, so the capability gate added in #2262 + // reads "unclassified" and forwards Codex's OpenAI-only `web_search` config fields. xAI + // rejects the whole request before inference ("Argument not supported: + // external_web_access"), which killed every routed Grok turn on the Responses lane. + // enrichProviderFromRegistry() already fills this, but the request path resolves through + // routedProviderConfig() and never called it. + ...(provider.supportsOpenAiWebSearchToolFields === undefined + && registryEntry.supportsOpenAiWebSearchToolFields !== undefined + ? { supportsOpenAiWebSearchToolFields: registryEntry.supportsOpenAiWebSearchToolFields } + : {}), ...(provider.preserveResponsesReasoningContent === undefined && registryEntry.preserveResponsesReasoningContent !== undefined ? { preserveResponsesReasoningContent: registryEntry.preserveResponsesReasoningContent } : {}), diff --git a/tests/responses-routed-web-search-fields.test.ts b/tests/responses-routed-web-search-fields.test.ts index 7df6e24a63..f6dc65ac27 100644 --- a/tests/responses-routed-web-search-fields.test.ts +++ b/tests/responses-routed-web-search-fields.test.ts @@ -2,6 +2,7 @@ import { describe, expect, test } from "bun:test"; import { createResponsesPassthroughAdapter as createResponsesPassthroughAdapterProduction, stripOpenAiOnlyWebSearchFields } from "../src/adapters/openai-responses"; import { enrichProviderFromRegistry, providerConfigSeed } from "../src/providers/derive"; import { getProviderRegistryEntry } from "../src/providers/registry"; +import { routedProviderConfig } from "../src/router"; import type { OcxProviderConfig } from "../src/types"; import { withTestTranslatorBudget } from "./helpers/translator-budget"; @@ -81,3 +82,52 @@ describe("Responses buildRequest web_search capability", () => { }]); }); }); + +// The request path resolves a saved provider row through routedProviderConfig(), NOT through +// enrichProviderFromRegistry(). Until this backfill existed, a saved xai row reached the +// Responses adapter with supportsOpenAiWebSearchToolFields === undefined, so the #2262 +// capability gate read "unclassified" and forwarded the fields; live xAI answered +// `400 Argument not supported: external_web_access` and every routed Grok turn on the +// Responses lane died before inference (verified against cli-chat-proxy.grok.com 2026-08-21). +// Asserting on the adapter with a hand-built provider cannot catch this — the gap is upstream +// of the adapter, in what the router hands it. +describe("routedProviderConfig web_search capability backfill", () => { + test("a saved xai row without the flag is classified by the registry", () => { + const saved: OcxProviderConfig = { + adapter: "openai-chat", + baseUrl: "https://api.x.ai/v1", + authMode: "oauth", + // The GUI Responses opt-in writes only modelAdapters; it never writes the capability. + modelAdapters: { "grok-4.6": "openai-responses" }, + }; + expect(saved.supportsOpenAiWebSearchToolFields).toBeUndefined(); + + const routed = routedProviderConfig("xai", saved); + expect(routed.supportsOpenAiWebSearchToolFields).toBe(false); + }); + + test("the routed row actually strips the fatal fields at the adapter", () => { + const routed = routedProviderConfig("xai", { + adapter: "openai-chat", + baseUrl: "https://api.x.ai/v1", + authMode: "oauth", + modelAdapters: { "grok-4.6": "openai-responses" }, + }); + + const body = buildWebSearchBody({ ...routed, adapter: "openai-responses" }); + expect(body.tools).toEqual([{ + type: "web_search", + user_location: { type: "approximate" }, + }]); + }); + + test("an explicit saved value still overrides the registry default", () => { + const routed = routedProviderConfig("xai", { + adapter: "openai-responses", + baseUrl: "https://api.x.ai/v1", + authMode: "oauth", + supportsOpenAiWebSearchToolFields: true, + }); + expect(routed.supportsOpenAiWebSearchToolFields).toBe(true); + }); +});