From 47e56de301435f5eb7fe6898a927c7c6de9fb735 Mon Sep 17 00:00:00 2001 From: Bohdan Kremnyi Date: Fri, 4 Sep 2026 12:02:12 +0200 Subject: [PATCH 1/2] fix(responses): scope Muse web search compatibility --- src/adapters/openai-responses.ts | 22 +++++++++- tests/muse-spark-web-search-compat.test.ts | 47 ++++++++++++++++++++-- 2 files changed, 64 insertions(+), 5 deletions(-) diff --git a/src/adapters/openai-responses.ts b/src/adapters/openai-responses.ts index 297f3a980da..1305c9ffcea 100644 --- a/src/adapters/openai-responses.ts +++ b/src/adapters/openai-responses.ts @@ -2116,6 +2116,11 @@ const MUSE_SPARK_WEB_SEARCH_STRICT_MODELS = new Set([ "muse-spark-1.2-contributor", ]); +const MUSE_SPARK_WEB_SEARCH_STRICT_DESTINATIONS = new Set([ + "https://opencode.ai/zen/v1", + "https://opencode.ai/zen/go/v1", +]); + const MUSE_SPARK_UNSUPPORTED_WEB_SEARCH_FIELDS = [ "search_content_types", "indexed_web_access", @@ -2127,10 +2132,23 @@ const MUSE_SPARK_UNSUPPORTED_WEB_SEARCH_FIELDS = [ * remains untouched. Keep the rejected names together so a newly identified field * is a one-line compatibility update rather than another bespoke rewrite. */ -function stripMuseSparkUnsupportedWebSearchFields(body: unknown, modelId: unknown): unknown { +function stripMuseSparkUnsupportedWebSearchFields( + body: unknown, + modelId: unknown, + provider: Pick, +): unknown { if (!isPlainObject(body)) return body; if (typeof modelId !== "string") return body; if (!MUSE_SPARK_WEB_SEARCH_STRICT_MODELS.has(modelId.trim().toLowerCase())) return body; + let destination: string; + try { + const url = new URL(provider.baseUrl); + if (url.username || url.password || url.search || url.hash) return body; + destination = `${url.origin.toLowerCase()}${url.pathname.replace(/\/+$/, "")}`; + } catch { + return body; + } + if (!MUSE_SPARK_WEB_SEARCH_STRICT_DESTINATIONS.has(destination)) return body; const rewriteTools = (tools: unknown[]): { tools: unknown[]; changed: boolean } => { let changed = false; @@ -2409,7 +2427,7 @@ export function createResponsesPassthroughAdapter(provider: OcxProviderConfig): if (provider.supportsOpenAiWebSearchToolFields === false) { outBody = stripOpenAiOnlyWebSearchFields(outBody); } - outBody = stripMuseSparkUnsupportedWebSearchFields(outBody, parsed.modelId); + outBody = stripMuseSparkUnsupportedWebSearchFields(outBody, parsed.modelId, provider); // Last, so promoted namespace children are also cleared of Codex-private fields. outBody = stripCanonicalOnlyToolFields(outBody, provider.supportsOpenAiWebSearchToolFields === false); } diff --git a/tests/muse-spark-web-search-compat.test.ts b/tests/muse-spark-web-search-compat.test.ts index 08bb3a74fac..2667cb574b3 100644 --- a/tests/muse-spark-web-search-compat.test.ts +++ b/tests/muse-spark-web-search-compat.test.ts @@ -7,12 +7,22 @@ import { withTestTranslatorBudget } from "./helpers/translator-budget"; const createResponsesPassthroughAdapter = (...args: Parameters) => withTestTranslatorBudget(createResponsesPassthroughAdapterProduction(...args)); -const PROVIDER = { +const ZEN_PROVIDER = { adapter: "openai-responses", baseUrl: "https://opencode.ai/zen/v1", apiKey: "test-key", } as unknown as OcxProviderConfig; +const ZEN_GO_PROVIDER = { + ...ZEN_PROVIDER, + baseUrl: "https://opencode.ai/zen/go/v1", +}; + +const META_PROVIDER = { + ...ZEN_PROVIDER, + baseUrl: "https://api.meta.ai/v1", +}; + /** A Codex web_search declaration exactly as `hosted_spec.rs` emits it for TextAndImage. */ function webSearchTool(): Record { return { @@ -23,8 +33,12 @@ function webSearchTool(): Record { }; } -function build(modelId: string, rawBody: Record): Record { - const request = createResponsesPassthroughAdapter(PROVIDER).buildRequest({ +function buildForProvider( + provider: OcxProviderConfig, + modelId: string, + rawBody: Record, +): Record { + const request = createResponsesPassthroughAdapter(provider).buildRequest({ modelId, context: { messages: [] }, stream: true, @@ -34,6 +48,10 @@ function build(modelId: string, rawBody: Record): Record; } +function build(modelId: string, rawBody: Record): Record { + return buildForProvider(ZEN_PROVIDER, modelId, rawBody); +} + const toolsOf = (body: Record) => body.tools as Array>; /** @@ -125,4 +143,27 @@ describe("#2617/#3378 Muse Spark web_search compatibility", () => { expect(Object.hasOwn(nested, "search_content_types")).toBe(false); expect(Object.hasOwn(nested, "indexed_web_access")).toBe(false); }); + + test("OpenCode Go applies the same Muse compatibility guard", () => { + const body = buildForProvider(ZEN_GO_PROVIDER, "muse-spark-1.3-contributor", { + tools: [webSearchTool()], + }); + const tool = toolsOf(body)[0]!; + expect(Object.hasOwn(tool, "search_content_types")).toBe(false); + expect(Object.hasOwn(tool, "indexed_web_access")).toBe(false); + }); + + test("direct Meta preserves its web_search fields at both tool positions", () => { + const body = buildForProvider(META_PROVIDER, "muse-spark-1.3-contributor", { + tools: [webSearchTool()], + input: [{ type: "additional_tools", tools: [webSearchTool()] }], + }); + const tool = toolsOf(body)[0]!; + const item = (body.input as Array>)[0]!; + const nested = (item.tools as Array>)[0]!; + for (const declaration of [tool, nested]) { + expect(declaration.search_content_types).toEqual(["text", "image"]); + expect(declaration.indexed_web_access).toBe(true); + } + }); }); From bf422afcfd01b1581fbb78a69973d822d3d90048 Mon Sep 17 00:00:00 2001 From: Bohdan Kremnyi Date: Fri, 4 Sep 2026 12:22:45 +0200 Subject: [PATCH 2/2] fix(responses): derive Muse scope from request URL --- src/adapters/openai-responses.ts | 20 +++++++++-------- tests/muse-spark-web-search-compat.test.ts | 25 ++++++++++++++++++++++ 2 files changed, 36 insertions(+), 9 deletions(-) diff --git a/src/adapters/openai-responses.ts b/src/adapters/openai-responses.ts index 1305c9ffcea..d46bd5d80a0 100644 --- a/src/adapters/openai-responses.ts +++ b/src/adapters/openai-responses.ts @@ -2116,9 +2116,9 @@ const MUSE_SPARK_WEB_SEARCH_STRICT_MODELS = new Set([ "muse-spark-1.2-contributor", ]); -const MUSE_SPARK_WEB_SEARCH_STRICT_DESTINATIONS = new Set([ - "https://opencode.ai/zen/v1", - "https://opencode.ai/zen/go/v1", +const MUSE_SPARK_WEB_SEARCH_STRICT_RESPONSE_URLS = new Set([ + "https://opencode.ai/zen/v1/responses", + "https://opencode.ai/zen/go/v1/responses", ]); const MUSE_SPARK_UNSUPPORTED_WEB_SEARCH_FIELDS = [ @@ -2129,26 +2129,28 @@ const MUSE_SPARK_UNSUPPORTED_WEB_SEARCH_FIELDS = [ /** * OpenCode Zen / Go Muse Spark Responses gateway refuses a short list of Codex * `web_search` fields. `web_search_preview` keeps its accepted shape, and Luna - * remains untouched. Keep the rejected names together so a newly identified field - * is a one-line compatibility update rather than another bespoke rewrite. + * remains untouched. Match the exact effective request URL; malformed, credentialed, + * or parameterized destinations keep their original body instead of assuming this + * gateway contract. Keep the rejected names together so a newly identified field is + * a one-line compatibility update rather than another bespoke rewrite. */ function stripMuseSparkUnsupportedWebSearchFields( body: unknown, modelId: unknown, - provider: Pick, + responseUrl: string, ): unknown { if (!isPlainObject(body)) return body; if (typeof modelId !== "string") return body; if (!MUSE_SPARK_WEB_SEARCH_STRICT_MODELS.has(modelId.trim().toLowerCase())) return body; let destination: string; try { - const url = new URL(provider.baseUrl); + const url = new URL(responseUrl); if (url.username || url.password || url.search || url.hash) return body; destination = `${url.origin.toLowerCase()}${url.pathname.replace(/\/+$/, "")}`; } catch { return body; } - if (!MUSE_SPARK_WEB_SEARCH_STRICT_DESTINATIONS.has(destination)) return body; + if (!MUSE_SPARK_WEB_SEARCH_STRICT_RESPONSE_URLS.has(destination)) return body; const rewriteTools = (tools: unknown[]): { tools: unknown[]; changed: boolean } => { let changed = false; @@ -2427,7 +2429,7 @@ export function createResponsesPassthroughAdapter(provider: OcxProviderConfig): if (provider.supportsOpenAiWebSearchToolFields === false) { outBody = stripOpenAiOnlyWebSearchFields(outBody); } - outBody = stripMuseSparkUnsupportedWebSearchFields(outBody, parsed.modelId, provider); + outBody = stripMuseSparkUnsupportedWebSearchFields(outBody, parsed.modelId, url); // Last, so promoted namespace children are also cleared of Codex-private fields. outBody = stripCanonicalOnlyToolFields(outBody, provider.supportsOpenAiWebSearchToolFields === false); } diff --git a/tests/muse-spark-web-search-compat.test.ts b/tests/muse-spark-web-search-compat.test.ts index 2667cb574b3..04e0f56a73c 100644 --- a/tests/muse-spark-web-search-compat.test.ts +++ b/tests/muse-spark-web-search-compat.test.ts @@ -18,6 +18,18 @@ const ZEN_GO_PROVIDER = { baseUrl: "https://opencode.ai/zen/go/v1", }; +const ZEN_PATH_PROVIDER = { + ...ZEN_PROVIDER, + baseUrl: "https://opencode.ai", + responsesPath: "/zen/v1/responses", +}; + +const ZEN_GO_PATH_PROVIDER = { + ...ZEN_PROVIDER, + baseUrl: "https://opencode.ai", + responsesPath: "/zen/go/v1/responses", +}; + const META_PROVIDER = { ...ZEN_PROVIDER, baseUrl: "https://api.meta.ai/v1", @@ -33,6 +45,7 @@ function webSearchTool(): Record { }; } +/** Build one passthrough request for an explicit Responses provider fixture. */ function buildForProvider( provider: OcxProviderConfig, modelId: string, @@ -48,6 +61,7 @@ function buildForProvider( return JSON.parse(request.body) as Record; } +/** Build with the default OpenCode Zen fixture used by the original regressions. */ function build(modelId: string, rawBody: Record): Record { return buildForProvider(ZEN_PROVIDER, modelId, rawBody); } @@ -153,6 +167,17 @@ describe("#2617/#3378 Muse Spark web_search compatibility", () => { expect(Object.hasOwn(tool, "indexed_web_access")).toBe(false); }); + test("split baseUrl and responsesPath configurations derive both strict destinations", () => { + for (const provider of [ZEN_PATH_PROVIDER, ZEN_GO_PATH_PROVIDER]) { + const body = buildForProvider(provider, "muse-spark-1.3-contributor", { + tools: [webSearchTool()], + }); + const tool = toolsOf(body)[0]!; + expect(Object.hasOwn(tool, "search_content_types")).toBe(false); + expect(Object.hasOwn(tool, "indexed_web_access")).toBe(false); + } + }); + test("direct Meta preserves its web_search fields at both tool positions", () => { const body = buildForProvider(META_PROVIDER, "muse-spark-1.3-contributor", { tools: [webSearchTool()],