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
11 changes: 11 additions & 0 deletions src/router.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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 }
: {}),
Expand Down
50 changes: 50 additions & 0 deletions tests/responses-routed-web-search-fields.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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";

Expand Down Expand Up @@ -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);
});
});
Loading