From dc02382e600173752cabb5ea49e81f53b78596fb Mon Sep 17 00:00:00 2001 From: jun Date: Thu, 3 Sep 2026 09:58:51 +0900 Subject: [PATCH] feat(models): add Muse Spark 1.3 on the 1.2 spec MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Meta shipped Muse Spark 1.3 on 2026-09-02. Command Code publishes meta/muse-spark-1.3 and meta/muse-spark-1.3-contributor alongside the 1.2 pair, and OpenCode Zen serves muse-spark-1.3-contributor (Go) and muse-spark-1.3-contributor-free (Zen) on the same wire as their 1.2 siblings. Additive, not a replacement: every 1.2 and 1.1 id stays exactly where it is. 1.3 inherits the 1.2 spec on every surface that already named 1.2, because it is the same-shaped successor on the same gateway. Four surfaces move together, and three of them would have failed silently had 1.3 only been added to the first: - Effort ladder (low/medium/high/xhigh/max) for both 1.3 ids, so a client that sends an effort gets it forwarded rather than stripped. - Command Code image models, so the catalog does not advertise a multimodal model as text-only. - OpenCode Go registry: Responses wire default, the 1M context window, and text+image. Zen Go publishes no modality or window metadata, so without these declarations 1.3 regresses to the 128k default and the Codex app blocks image attachments client-side — the exact pair of defects the 1.2 entries were added to fix. - The Muse web_search guard was an equality check on the 1.2 id. Selecting 1.3 would have let Codex's search_content_types field reach a gateway that 400s it. It is now a membership set. Verification: focused bun test on the four Muse/Command Code suites (55 pass) plus bun run typecheck. The repository-wide local suite was not run at the user's instruction; CI is the full gate. --- src/adapters/openai-responses.ts | 15 +++++++++- src/providers/command-code-efforts.ts | 15 ++++++++++ src/providers/registry.ts | 16 ++++++++-- tests/command-code-provider.test.ts | 9 ++++++ tests/muse-spark-web-search-compat.test.ts | 34 ++++++++++++++++++++++ tests/opencode-go-muse-context.test.ts | 16 ++++++++++ tests/opencode-go-muse-vision.test.ts | 22 ++++++++++++++ 7 files changed, 123 insertions(+), 4 deletions(-) diff --git a/src/adapters/openai-responses.ts b/src/adapters/openai-responses.ts index 781bbef414..8e23b383f3 100644 --- a/src/adapters/openai-responses.ts +++ b/src/adapters/openai-responses.ts @@ -1965,6 +1965,18 @@ export function stripOpenAiOnlyWebSearchFields(body: unknown): unknown { return changed ? next : body; } +/** + * Muse Spark ids whose Responses gateway refuses `search_content_types` on a plain + * `web_search` tool. Membership, not equality: 1.3 shipped 2026-09-02 as the + * same-shaped successor to 1.2 on the same Zen wire, and an equality check would + * have let a Codex-emitted `web_search` + `search_content_types` body reach the + * gateway and come back 400 for every request the moment 1.3 was selected. + */ +const MUSE_SPARK_WEB_SEARCH_STRICT_MODELS = new Set([ + "muse-spark-1.3-contributor", + "muse-spark-1.2-contributor", +]); + /** * OpenCode Zen / Go Muse Spark Responses gateway refuses `search_content_types` * on a plain `web_search` tool (400) but accepts it on `web_search_preview`; a @@ -1976,7 +1988,8 @@ export function stripOpenAiOnlyWebSearchFields(body: unknown): unknown { */ function stripMuseSparkUnsupportedWebSearchFields(body: unknown, modelId: unknown): unknown { if (!isPlainObject(body)) return body; - if (typeof modelId !== "string" || modelId.trim().toLowerCase() !== "muse-spark-1.2-contributor") return body; + if (typeof modelId !== "string") return body; + if (!MUSE_SPARK_WEB_SEARCH_STRICT_MODELS.has(modelId.trim().toLowerCase())) return body; const rewriteTools = (tools: unknown[]): { tools: unknown[]; changed: boolean } => { let changed = false; diff --git a/src/providers/command-code-efforts.ts b/src/providers/command-code-efforts.ts index b5bfa93b37..621eb330bc 100644 --- a/src/providers/command-code-efforts.ts +++ b/src/providers/command-code-efforts.ts @@ -103,6 +103,21 @@ const COMMAND_CODE_MODEL_EFFORTS = { // 2026-08-13: direct upstream POST with low/medium/high/xhigh/max all 200, // ultra 400; reasoningTokens differentiated 114..253; proxy previously stripped // the field so effort changes had no effect). + // + // 1.3 shipped 2026-09-02 as the same-shaped successor to 1.2 (Command Code + // publishes meta/muse-spark-1.3 and meta/muse-spark-1.3-contributor alongside + // the 1.2 pair, and Zen serves muse-spark-1.3-contributor over the same + // /responses wire). It carries the 1.2 ladder because it IS the 1.2 spec: the + // upstream ladder statement is per-family, and a narrower guess here would + // strip an effort the gateway accepts. Additive — 1.2 and 1.1 stay live. + "meta/muse-spark-1.3": { + efforts: ["low", "medium", "high", "xhigh", "max"], + profileUrl: "https://commandcode.ai/models/meta-muse-spark-1.3", + }, + "meta/muse-spark-1.3-contributor": { + efforts: ["low", "medium", "high", "xhigh", "max"], + profileUrl: "https://commandcode.ai/models/meta-muse-spark-1.3-contributor", + }, "meta/muse-spark-1.2": { efforts: ["low", "medium", "high", "xhigh", "max"], profileUrl: "https://commandcode.ai/models/meta-muse-spark-1.2", diff --git a/src/providers/registry.ts b/src/providers/registry.ts index 1b9ddf4d5b..01b4a0fa1c 100644 --- a/src/providers/registry.ts +++ b/src/providers/registry.ts @@ -552,6 +552,8 @@ const COMMAND_CODE_IMAGE_MODELS = [ "gpt-5.6-sol", "MiniMaxAI/MiniMax-M3", "moonshotai/Kimi-K3", + "meta/muse-spark-1.3", + "meta/muse-spark-1.3-contributor", "meta/muse-spark-1.2", "meta/muse-spark-1.2-contributor", ] as const; @@ -1487,25 +1489,33 @@ export const PROVIDER_REGISTRY: readonly ProviderRegistryEntry[] = [ - 다른 대안 대신 이 방식을 선택한 이유: OpenCode Go documents sibling models on Chat or Anthropic endpoints, and an exact registry default preserves both those routes and explicit opt-out precedence. - 장점, 단점 및 영향: Each listed model reaches `/responses` from every inbound surface without changing siblings; a future upstream endpoint change requires an evidence-backed registry update. */ - modelWireDefaults: { "gpt-5.6-luna": "openai-responses", "muse-spark-1.2-contributor": "openai-responses" }, + modelWireDefaults: { + "gpt-5.6-luna": "openai-responses", + "muse-spark-1.3-contributor": "openai-responses", + "muse-spark-1.2-contributor": "openai-responses", + }, modelContextWindows: { "kimi-k3": KIMI_K3_STANDARD_CONTEXT_WINDOW, // The DeepSeek vision preview id is metadata-only here: the Go roster is // discovered live, so it applies the moment the gateway serves the id. [DEEPSEEK_VISION_PREVIEW_MODEL]: 1_048_576, - // Muse Spark 1.2 Contributor serves a 1,048,576-token (1M) context window over + // Muse Spark Contributor serves a 1,048,576-token (1M) context window over // /responses on Zen Go, matching its 1.1 sibling (Meta developer docs, verified 2026-08-28). // Without this declaration the catalog falls back to 128k, capping real usable context. + // 1.3 ships the same window as 1.2 and is served from the same Zen Go roster. + "muse-spark-1.3-contributor": 1_048_576, "muse-spark-1.2-contributor": 1_048_576, }, modelInputModalities: { "kimi-k3": ["text", "image"], // Experimental DeepSeek vision preview — expected to merge into deepseek-v4-flash later. [DEEPSEEK_VISION_PREVIEW_MODEL]: ["text", "image"], - // Muse Spark 1.2 Contributor is natively multimodal on Zen Go: it accepts input_image + // Muse Spark Contributor is natively multimodal on Zen Go: it accepts input_image // parts over /responses (probed 2026-08-26). Without this declaration the catalog // advertises it text-only and the Codex app blocks image attachments client-side with // "This model does not support image inputs" before the request ever reaches the proxy. + // 1.3 is the same-shaped successor and Command Code documents it as multimodal. + "muse-spark-1.3-contributor": ["text", "image"], "muse-spark-1.2-contributor": ["text", "image"], }, modelReasoningEfforts: { diff --git a/tests/command-code-provider.test.ts b/tests/command-code-provider.test.ts index f434687a1c..76aed55ccf 100644 --- a/tests/command-code-provider.test.ts +++ b/tests/command-code-provider.test.ts @@ -121,6 +121,8 @@ describe("Command Code provider", () => { "gpt-5.6-sol", "MiniMaxAI/MiniMax-M3", "moonshotai/Kimi-K3", + "meta/muse-spark-1.3", + "meta/muse-spark-1.3-contributor", "meta/muse-spark-1.2", "meta/muse-spark-1.2-contributor", ]; @@ -384,6 +386,13 @@ describe("Command Code provider", () => { expect(commandCodeReasoningEfforts("meta/muse-spark-1.2-contributor")).toEqual( ["low", "medium", "high", "xhigh", "max"], ); + // 1.3 shipped as the same-shaped successor and carries the identical ladder. + expect(commandCodeReasoningEfforts("meta/muse-spark-1.3-contributor")).toEqual( + ["low", "medium", "high", "xhigh", "max"], + ); + expect(commandCodeReasoningEfforts("meta/muse-spark-1.3")).toEqual( + ["low", "medium", "high", "xhigh", "max"], + ); expect(commandCodeReasoningEfforts("meta/muse-spark-1.2")).toEqual( ["low", "medium", "high", "xhigh", "max"], ); diff --git a/tests/muse-spark-web-search-compat.test.ts b/tests/muse-spark-web-search-compat.test.ts index 59b589178a..e8f7dc491b 100644 --- a/tests/muse-spark-web-search-compat.test.ts +++ b/tests/muse-spark-web-search-compat.test.ts @@ -80,8 +80,42 @@ describe("#2617 Muse Spark web_search compatibility", () => { test("the registry routes only the named exact models to Responses", () => { const defaults = getProviderRegistryEntry("opencode-go")?.modelWireDefaults ?? {}; + expect(defaults["muse-spark-1.3-contributor"]).toBe("openai-responses"); expect(defaults["muse-spark-1.2-contributor"]).toBe("openai-responses"); // An exact-model allowlist, not a family rule: a sibling must not be dragged along. expect(defaults["muse-spark-1.2"]).toBeUndefined(); + expect(defaults["muse-spark-1.3"]).toBeUndefined(); + }); + + /** + * 1.3 shipped 2026-09-02 on the same Zen wire with the same spec as 1.2. The guard + * used to be an equality check on the 1.2 id, so selecting 1.3 would have sent the + * refused field straight through and 400ed every Codex web_search request. + */ + test("Muse Spark 1.3 Contributor gets the same web_search sanitization", () => { + const body = build("muse-spark-1.3-contributor", { tools: [webSearchTool()] }); + const tool = toolsOf(body)[0]!; + expect(tool.type).toBe("web_search"); + expect(tool.search_context_size).toBe("medium"); + expect(Object.hasOwn(tool, "search_content_types")).toBe(false); + }); + + test("1.3 keeps the field on web_search_preview, where the gateway accepts it", () => { + const body = build("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"]); + }); + + test("a nested additional_tools declaration is sanitized for 1.3 too", () => { + const body = build("muse-spark-1.3-contributor", { + input: [{ type: "additional_tools", tools: [webSearchTool()] }], + }); + const item = (body.input as Array>)[0]!; + const nested = (item.tools as Array>)[0]!; + expect(nested.type).toBe("web_search"); + expect(Object.hasOwn(nested, "search_content_types")).toBe(false); }); }); diff --git a/tests/opencode-go-muse-context.test.ts b/tests/opencode-go-muse-context.test.ts index a387696d71..56bdc9dcc2 100644 --- a/tests/opencode-go-muse-context.test.ts +++ b/tests/opencode-go-muse-context.test.ts @@ -14,6 +14,7 @@ import { providerConfigSeed } from "../src/providers/derive"; import type { OcxProviderConfig } from "../src/types"; const MUSE_MODEL = "muse-spark-1.2-contributor"; +const MUSE_13_MODEL = "muse-spark-1.3-contributor"; const MUSE_CONTEXT = 1_048_576; /** Seeded OpenCode Go provider config for the Muse Spark context assertions. */ @@ -51,4 +52,19 @@ describe("OpenCode Go Muse Spark context window", () => { }); expect(hinted.contextWindow).toBe(MUSE_CONTEXT); }); + + // 1.3 is the same-shaped successor on the same Zen Go roster. Without its own + // entry it would fall back to the 128k unknown-window default — the same + // regression these tests exist to prevent for 1.2. + test("Muse Spark 1.3 Contributor declares and exposes the same 1M window", () => { + const entry = PROVIDER_REGISTRY.find(e => e.id === "opencode-go"); + expect(entry?.modelContextWindows?.[MUSE_13_MODEL]).toBe(MUSE_CONTEXT); + const prov = opencodeGo(); + expect(prov.modelContextWindows?.[MUSE_13_MODEL]).toBe(MUSE_CONTEXT); + const hinted = applyProviderConfigHints("opencode-go", prov, { + id: MUSE_13_MODEL, + provider: "opencode-go", + }); + expect(hinted.contextWindow).toBe(MUSE_CONTEXT); + }); }); diff --git a/tests/opencode-go-muse-vision.test.ts b/tests/opencode-go-muse-vision.test.ts index d915b9292a..b8eca82a6c 100644 --- a/tests/opencode-go-muse-vision.test.ts +++ b/tests/opencode-go-muse-vision.test.ts @@ -15,6 +15,7 @@ import { providerConfigSeed } from "../src/providers/derive"; import type { OcxProviderConfig } from "../src/types"; const MUSE_MODEL = "muse-spark-1.2-contributor"; +const MUSE_13_MODEL = "muse-spark-1.3-contributor"; /** Seeded OpenCode Go provider config for the Muse Spark vision assertions. */ function opencodeGo(): OcxProviderConfig { @@ -73,4 +74,25 @@ describe("OpenCode Go Muse Spark image input (#vision)", () => { }); expect(hinted.inputModalities).toEqual(["text", "image"]); }); + + // 1.3 shipped 2026-09-02 on the same Zen Go roster with the same spec as 1.2. + // Zen publishes no modality metadata, so without its own declaration the newer + // model would regress to the exact text-only block 1.2 was fixed for. + test("Muse Spark 1.3 Contributor carries the same text+image declaration", () => { + const entry = PROVIDER_REGISTRY.find(e => e.id === "opencode-go"); + expect(entry?.modelInputModalities?.[MUSE_13_MODEL]).toEqual(["text", "image"]); + const prov = opencodeGo(); + expect(prov.modelInputModalities?.[MUSE_13_MODEL]).toEqual(["text", "image"]); + expect(prov.noVisionModels ?? []).not.toContain(MUSE_13_MODEL); + }); + + test("1.3's configured declaration overrides a text-only discovered row", () => { + const prov = opencodeGo(); + const hinted = applyProviderConfigHints("opencode-go", prov, { + id: MUSE_13_MODEL, + provider: "opencode-go", + inputModalities: ["text"], + }); + expect(hinted.inputModalities).toEqual(["text", "image"]); + }); });