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
9 changes: 7 additions & 2 deletions src/adapters/xai-web-search.ts
Original file line number Diff line number Diff line change
Expand Up @@ -60,10 +60,15 @@ function normalizeToolGroup(tools: unknown[]): ToolGroupRewrite {
: undefined;
const enableImageSearch = searchContentTypes?.includes("image") === true;
const next: Record<string, unknown> = { ...tool, type: CODEX_WEB_SEARCH_TOOL };
// Only the two fields xAI actually refuses are removed. Probed 2026-08-22, one field per
// request, against BOTH xAI destinations (api.x.ai and cli-chat-proxy.grok.com): they behave
// identically — `external_web_access` 400s on every value including `true`, and
// `search_context_size` 400s, while `user_location`, `search_content_types`, `filters` and
// `enable_image_search` are all accepted. Deleting the accepted ones was a silent capability
// loss, and it contradicted the sibling layer, whose own probe note already records
// user_location/filters as accepted (tests/responses-routed-web-search-fields.test.ts).
delete next.external_web_access;
delete next.search_context_size;
delete next.search_content_types;
delete next.user_location;
if (enableImageSearch && !Object.hasOwn(next, "enable_image_search")) {
next.enable_image_search = true;
}
Expand Down
51 changes: 46 additions & 5 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 { resolveProviderTransport } from "../src/providers/xai-transport";
import { routedProviderConfig } from "../src/router";
import type { OcxProviderConfig } from "../src/types";
import { withTestTranslatorBudget } from "./helpers/translator-budget";
Expand All @@ -23,6 +24,8 @@ function buildWebSearchBody(provider: OcxProviderConfig): Record<string, unknown
external_web_access: true,
search_context_size: "medium",
user_location: { type: "approximate" },
search_content_types: ["text"],
filters: { allowed_domains: ["x.ai"] },
}],
},
}, { headers: new Headers() });
Expand Down Expand Up @@ -92,17 +95,24 @@ describe("Responses buildRequest web_search capability", () => {
external_web_access: true,
search_context_size: "medium",
user_location: { type: "approximate" },
search_content_types: ["text"],
filters: { allowed_domains: ["x.ai"] },
}]);
});

test("registry xAI traffic normalizes Codex search fields for its public Responses API", () => {
test("registry xAI traffic keeps accepted fields on the public api.x.ai Responses API", () => {
const entry = getProviderRegistryEntry("xai");
if (!entry) throw new Error("xAI registry entry missing");
const provider = { ...providerConfigSeed(entry), adapter: "openai-responses" };
enrichProviderFromRegistry("xai", provider);

const body = buildWebSearchBody(provider);
expect(body.tools).toEqual([{ type: "web_search" }]);
expect(body.tools).toEqual([{
type: "web_search",
user_location: { type: "approximate" },
search_content_types: ["text"],
filters: { allowed_domains: ["x.ai"] },
}]);
});

test("non-xAI classified gateways use generic field stripping, not xAI cached-search policy", () => {
Expand Down Expand Up @@ -184,16 +194,47 @@ describe("routedProviderConfig web_search capability backfill", () => {
expect(routed.supportsOpenAiWebSearchToolFields).toBe(false);
});

test("the routed row actually normalizes the search tool at the adapter", () => {
test("the registry-classified OAuth row strips only fatal fields at the CLI adapter", () => {
const routed = routedProviderConfig("xai", {
adapter: "openai-chat",
baseUrl: "https://api.x.ai/v1",
authMode: "oauth",
modelAdapters: { "grok-4.6": "openai-responses" },
});
const transport = resolveProviderTransport("xai", routed);

expect(transport.baseUrl).toBe("https://cli-chat-proxy.grok.com/v1");
const body = buildWebSearchBody({ ...transport, adapter: "openai-responses" });
expect(body.tools).toEqual([{
type: "web_search",
user_location: { type: "approximate" },
search_content_types: ["text"],
filters: { allowed_domains: ["x.ai"] },
}]);
});

const body = buildWebSearchBody({ ...routed, adapter: "openai-responses" });
expect(body.tools).toEqual([{ type: "web_search" }]);
test("an equivalent unclassified OAuth row retains fatal fields at the CLI adapter", () => {
const routed = routedProviderConfig("xai", {
adapter: "openai-chat",
baseUrl: "https://api.x.ai/v1",
authMode: "oauth",
modelAdapters: { "grok-4.6": "openai-responses" },
});
const unclassified = { ...routed };
delete unclassified.supportsOpenAiWebSearchToolFields;
const transport = resolveProviderTransport("xai", unclassified);

expect(transport.baseUrl).toBe("https://cli-chat-proxy.grok.com/v1");
expect(transport.supportsOpenAiWebSearchToolFields).toBeUndefined();
const body = buildWebSearchBody({ ...transport, adapter: "openai-responses" });
expect(body.tools).toEqual([{
type: "web_search",
external_web_access: true,
search_context_size: "medium",
user_location: { type: "approximate" },
search_content_types: ["text"],
filters: { allowed_domains: ["x.ai"] },
}]);
});

test("an explicit saved value still overrides the registry default", () => {
Expand Down
11 changes: 8 additions & 3 deletions tests/xai-web-search-compat.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,12 @@ function buildBody(rawBody: Record<string, unknown>): Record<string, unknown> {
}

describe("xAI Responses web-search compatibility", () => {
test("lowers Codex live-search fields to xAI's documented tool schema", () => {
// Probed 2026-08-22, one field per request, against BOTH xAI destinations (api.x.ai and
// cli-chat-proxy.grok.com), which behave identically: external_web_access 400s on every value
// including `true`, search_context_size 400s, and user_location / search_content_types /
// filters / enable_image_search are all accepted. Only the two refused fields are removed;
// deleting the accepted ones was a silent capability loss.
test("removes only the fields xAI refuses and keeps the accepted ones", () => {
const body = buildBody({
model: "grok-4.6",
input: "latest xAI news",
Expand All @@ -42,13 +47,13 @@ describe("xAI Responses web-search compatibility", () => {
expect(body.tools).toEqual([{
type: "web_search",
filters: { allowed_domains: ["x.ai"] },
user_location: { type: "approximate", country: "KR" },
search_content_types: ["text", "image"],
enable_image_search: true,
}]);
expect(body.tool_choice).toEqual({ type: "web_search" });
expect(JSON.stringify(body)).not.toContain("external_web_access");
expect(JSON.stringify(body)).not.toContain("search_context_size");
expect(JSON.stringify(body)).not.toContain("search_content_types");
expect(JSON.stringify(body)).not.toContain("user_location");
});

test("omits cached-only search instead of silently widening it to xAI live search", () => {
Expand Down
Loading