diff --git a/.github/pr-assets/muse-spark-meta-search-content-types-400.jpg b/.github/pr-assets/muse-spark-meta-search-content-types-400.jpg new file mode 100644 index 0000000000..d18dd98dab Binary files /dev/null and b/.github/pr-assets/muse-spark-meta-search-content-types-400.jpg differ diff --git a/src/adapters/openai-responses.ts b/src/adapters/openai-responses.ts index 60c679489d..7498722204 100644 --- a/src/adapters/openai-responses.ts +++ b/src/adapters/openai-responses.ts @@ -2132,6 +2132,7 @@ const MUSE_SPARK_WEB_SEARCH_STRICT_MODELS = new Set([ const MUSE_SPARK_WEB_SEARCH_STRICT_RESPONSE_URLS = new Set([ "https://opencode.ai/zen/v1/responses", "https://opencode.ai/zen/go/v1/responses", + "https://api.meta.ai/v1/responses", ]); const MUSE_SPARK_UNSUPPORTED_WEB_SEARCH_FIELDS = [ @@ -2140,12 +2141,13 @@ const MUSE_SPARK_UNSUPPORTED_WEB_SEARCH_FIELDS = [ ] as const; /** - * 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. 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. + * OpenCode Zen / Go and the direct Meta Muse Spark Responses gateways refuse a + * short list of Codex `web_search` fields. `web_search_preview` keeps its accepted + * shape, and Luna 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, diff --git a/tests/providers/muse-spark-web-search-compat.test.ts b/tests/providers/muse-spark-web-search-compat.test.ts index 062a42ebcd..5254c7163d 100644 --- a/tests/providers/muse-spark-web-search-compat.test.ts +++ b/tests/providers/muse-spark-web-search-compat.test.ts @@ -35,6 +35,12 @@ const META_PROVIDER = { baseUrl: "https://api.meta.ai/v1", }; +const META_PATH_PROVIDER = { + ...ZEN_PROVIDER, + baseUrl: "https://api.meta.ai", + responsesPath: "/v1/responses", +}; + /** A Codex web_search declaration exactly as `hosted_spec.rs` emits it for TextAndImage. */ function webSearchTool(): Record { return { @@ -219,7 +225,14 @@ describe("#2617/#3378 Muse Spark web_search compatibility", () => { } }); - test("direct Meta preserves its web_search fields at both tool positions", () => { + /** + * #3456 scoped the guard to OpenCode Zen/Go URLs on the assumption that + * `https://api.meta.ai/v1` accepted Codex's extra web_search fields. Direct + * Meta still 400s `tools[].search_content_types` on ordinary `web_search` + * (live 2026-09-07 against muse-spark-1.3-contributor). Same model-id set, + * same field drop, same preview preservation. + */ + test("direct Meta strips rejected 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()] }], @@ -228,8 +241,29 @@ describe("#2617/#3378 Muse Spark web_search compatibility", () => { 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); + expect(declaration.type).toBe("web_search"); + expect(declaration.search_context_size).toBe("medium"); + expect(Object.hasOwn(declaration, "search_content_types")).toBe(false); + expect(Object.hasOwn(declaration, "indexed_web_access")).toBe(false); } }); + + test("direct Meta keeps the field on web_search_preview", () => { + const body = buildForProvider(META_PROVIDER, "muse-spark-1.3-contributor", { + tools: [{ ...webSearchTool(), type: "web_search_preview" }], + }); + const tool = toolsOf(body)[0]!; + expect(tool.type).toBe("web_search_preview"); + expect(tool.search_content_types).toEqual(["text", "image"]); + expect(tool.indexed_web_access).toBe(true); + }); + + test("split Meta baseUrl and responsesPath derives the same strict destination", () => { + const body = buildForProvider(META_PATH_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); + }); });