diff --git a/src/adapters/openai-responses.ts b/src/adapters/openai-responses.ts index 781bbef414..9209566801 100644 --- a/src/adapters/openai-responses.ts +++ b/src/adapters/openai-responses.ts @@ -1976,7 +1976,10 @@ 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; + const normalized = modelId.trim().toLowerCase(); + const slug = normalized.includes("/") ? normalized.split("/").pop()! : normalized; + if (!slug.startsWith("muse-spark")) return body; const rewriteTools = (tools: unknown[]): { tools: unknown[]; changed: boolean } => { let changed = false; diff --git a/src/providers/registry.ts b/src/providers/registry.ts index 1b9ddf4d5b..dbc0b9f4cc 100644 --- a/src/providers/registry.ts +++ b/src/providers/registry.ts @@ -1487,7 +1487,7 @@ 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.2-contributor": "openai-responses", "muse-spark-1.3-contributor": "openai-responses" }, modelContextWindows: { "kimi-k3": KIMI_K3_STANDARD_CONTEXT_WINDOW, // The DeepSeek vision preview id is metadata-only here: the Go roster is @@ -1497,6 +1497,7 @@ export const PROVIDER_REGISTRY: readonly ProviderRegistryEntry[] = [ // /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. "muse-spark-1.2-contributor": 1_048_576, + "muse-spark-1.3-contributor": 1_048_576, }, modelInputModalities: { "kimi-k3": ["text", "image"], @@ -1507,6 +1508,7 @@ export const PROVIDER_REGISTRY: readonly ProviderRegistryEntry[] = [ // 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. "muse-spark-1.2-contributor": ["text", "image"], + "muse-spark-1.3-contributor": ["text", "image"], }, modelReasoningEfforts: { "gpt-5.6-luna": OPENAI_API_GPT56_REASONING_EFFORTS, @@ -3040,7 +3042,14 @@ export function providerModelWireDefault( if (!allowedWires.has(provider.adapter)) return undefined; const entry = getProviderRegistryEntry(id); if (!entry?.modelWireDefaults || !providerMatchesRegistryTransport(id, provider)) return undefined; - const declared = entry.modelWireDefaults[modelId.trim().toLowerCase()]; + let declared = entry.modelWireDefaults[modelId.trim().toLowerCase()]; + if (declared === undefined) { + const trimmed = modelId.trim().toLowerCase(); + const slug = trimmed.includes("/") ? trimmed.split("/").pop()! : trimmed; + if (id === "opencode-go" && slug.startsWith("muse-spark")) { + declared = "openai-responses"; + } + } if (declared === undefined) return undefined; // A bare string applies to every inbound/auth mode; the object form may narrow either. if (typeof declared !== "string") { diff --git a/src/reasoning-effort.ts b/src/reasoning-effort.ts index 6342159d31..77e41c1939 100644 --- a/src/reasoning-effort.ts +++ b/src/reasoning-effort.ts @@ -124,6 +124,12 @@ export function modelRecordValue(record: Record | undefined, model for (const [key, value] of Object.entries(record)) { if (key.toLowerCase() === folded) return value; } + const slug = folded.includes("/") ? folded.split("/").pop()! : folded; + if (slug.startsWith("muse-spark")) { + for (const [key, value] of Object.entries(record)) { + if (key.toLowerCase().startsWith("muse-spark")) return value; + } + } return undefined; } diff --git a/src/server/adapter-resolve.ts b/src/server/adapter-resolve.ts index 8587b3191d..734d88208e 100644 --- a/src/server/adapter-resolve.ts +++ b/src/server/adapter-resolve.ts @@ -1,3 +1,4 @@ +import { modelRecordValue } from "../reasoning-effort"; import { createRegisteredAdapter } from "../adapters/registry"; import type { OcxProviderConfig } from "../types"; import { isWirePinnedModel, MODEL_ADAPTER_OVERRIDE_ALLOWED, pinnedWireAdapter } from "../types"; @@ -29,7 +30,7 @@ export function resolveWireProtocolOverride( } // Re-check the allow-list here, not just in the config validator: the file may have // been hand-edited, or written by a build that allowed more values. - const configured = providerConfig.modelAdapters?.[modelId]; + const configured = modelRecordValue(providerConfig.modelAdapters, modelId); // An explicit allowed override wins, including one naming the provider-wide adapter (the // opt-out from a registry default). Invalid hand-edited values fall through to the default. const requested = configured && MODEL_ADAPTER_OVERRIDE_ALLOWED.has(configured) diff --git a/tests/muse-spark-web-search-compat.test.ts b/tests/muse-spark-web-search-compat.test.ts index 59b589178a..a44f0e5d6c 100644 --- a/tests/muse-spark-web-search-compat.test.ts +++ b/tests/muse-spark-web-search-compat.test.ts @@ -46,6 +46,14 @@ const toolsOf = (body: Record) => body.tools as Array { + test("drops search_content_types for muse-spark-1.3-contributor too", () => { + 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("drops search_content_types from a plain web_search, keeping the tool and its other fields", () => { const body = build("muse-spark-1.2-contributor", { tools: [webSearchTool()] }); const tool = toolsOf(body)[0]!; diff --git a/tests/opencode-go-muse-context.test.ts b/tests/opencode-go-muse-context.test.ts index a387696d71..a40a40f9cc 100644 --- a/tests/opencode-go-muse-context.test.ts +++ b/tests/opencode-go-muse-context.test.ts @@ -13,7 +13,7 @@ import { getProviderRegistryEntry, PROVIDER_REGISTRY } from "../src/providers/re import { providerConfigSeed } from "../src/providers/derive"; import type { OcxProviderConfig } from "../src/types"; -const MUSE_MODEL = "muse-spark-1.2-contributor"; +const MUSE_MODELS = ["muse-spark-1.2-contributor", "muse-spark-1.3-contributor"] as const; const MUSE_CONTEXT = 1_048_576; /** Seeded OpenCode Go provider config for the Muse Spark context assertions. */ @@ -26,29 +26,37 @@ function opencodeGo(): OcxProviderConfig { describe("OpenCode Go Muse Spark context window", () => { test("registry declares the 1M context window for Muse", () => { const entry = PROVIDER_REGISTRY.find(e => e.id === "opencode-go"); - expect(entry?.modelContextWindows?.[MUSE_MODEL]).toBe(MUSE_CONTEXT); + for (const m of MUSE_MODELS) { + expect(entry?.modelContextWindows?.[m]).toBe(MUSE_CONTEXT); + } }); test("the registry seed carries the 1M context window for Muse", () => { const prov = opencodeGo(); - expect(prov.modelContextWindows?.[MUSE_MODEL]).toBe(MUSE_CONTEXT); + for (const m of MUSE_MODELS) { + expect(prov.modelContextWindows?.[m]).toBe(MUSE_CONTEXT); + } }); test("applyProviderConfigHints exposes the 1M context window for Muse", () => { const prov = opencodeGo(); - const hinted = applyProviderConfigHints("opencode-go", prov, { - id: MUSE_MODEL, - provider: "opencode-go", - }); - expect(hinted.contextWindow).toBe(MUSE_CONTEXT); + for (const m of MUSE_MODELS) { + const hinted = applyProviderConfigHints("opencode-go", prov, { + id: m, + provider: "opencode-go", + }); + expect(hinted.contextWindow).toBe(MUSE_CONTEXT); + } }); test("a discovered row with no window inherits the configured 1M window", () => { const prov = opencodeGo(); - const hinted = applyProviderConfigHints("opencode-go", prov, { - id: MUSE_MODEL, - provider: "opencode-go", - }); - expect(hinted.contextWindow).toBe(MUSE_CONTEXT); + for (const m of MUSE_MODELS) { + const hinted = applyProviderConfigHints("opencode-go", prov, { + id: m, + 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..a785e974f4 100644 --- a/tests/opencode-go-muse-vision.test.ts +++ b/tests/opencode-go-muse-vision.test.ts @@ -14,7 +14,7 @@ import { getProviderRegistryEntry, PROVIDER_REGISTRY } from "../src/providers/re import { providerConfigSeed } from "../src/providers/derive"; import type { OcxProviderConfig } from "../src/types"; -const MUSE_MODEL = "muse-spark-1.2-contributor"; +const MUSE_MODELS = ["muse-spark-1.2-contributor", "muse-spark-1.3-contributor"] as const; /** Seeded OpenCode Go provider config for the Muse Spark vision assertions. */ function opencodeGo(): OcxProviderConfig { @@ -26,27 +26,35 @@ function opencodeGo(): OcxProviderConfig { describe("OpenCode Go Muse Spark image input (#vision)", () => { test("registry declares Muse as text+image", () => { const entry = PROVIDER_REGISTRY.find(e => e.id === "opencode-go"); - expect(entry?.modelInputModalities?.[MUSE_MODEL]).toEqual(["text", "image"]); + for (const m of MUSE_MODELS) { + expect(entry?.modelInputModalities?.[m]).toEqual(["text", "image"]); + } }); test("the registry seed carries Muse as text+image", () => { const prov = opencodeGo(); - expect(prov.modelInputModalities?.[MUSE_MODEL]).toEqual(["text", "image"]); + for (const m of MUSE_MODELS) { + expect(prov.modelInputModalities?.[m]).toEqual(["text", "image"]); + } }); test("applyProviderConfigHints advertises image input for Muse", () => { const prov = opencodeGo(); - const hinted = applyProviderConfigHints("opencode-go", prov, { - id: MUSE_MODEL, - provider: "opencode-go", - }); - expect(hinted.inputModalities).toEqual(["text", "image"]); + for (const m of MUSE_MODELS) { + const hinted = applyProviderConfigHints("opencode-go", prov, { + id: m, + provider: "opencode-go", + }); + expect(hinted.inputModalities).toEqual(["text", "image"]); + } }); test("Muse is NOT in noVisionModels (it is natively multimodal, not sidecar-only)", () => { const prov = opencodeGo(); - expect(prov.noVisionModels ?? []).not.toContain(MUSE_MODEL); - expect(prov.modelInputModalities?.[MUSE_MODEL]).toEqual(["text", "image"]); + for (const m of MUSE_MODELS) { + expect(prov.noVisionModels ?? []).not.toContain(m); + expect(prov.modelInputModalities?.[m]).toEqual(["text", "image"]); + } }); // The registry declaration only matters if it survives a live discovery row that @@ -57,20 +65,24 @@ describe("OpenCode Go Muse Spark image input (#vision)", () => { // still blocked image attachments in production. test("the configured declaration overrides a text-only discovered row", () => { const prov = opencodeGo(); - const hinted = applyProviderConfigHints("opencode-go", prov, { - id: MUSE_MODEL, - provider: "opencode-go", - inputModalities: ["text"], - }); - expect(hinted.inputModalities).toEqual(["text", "image"]); + for (const m of MUSE_MODELS) { + const hinted = applyProviderConfigHints("opencode-go", prov, { + id: m, + provider: "opencode-go", + inputModalities: ["text"], + }); + expect(hinted.inputModalities).toEqual(["text", "image"]); + } }); test("the configured declaration fills in a discovered row with no modalities", () => { const prov = opencodeGo(); - const hinted = applyProviderConfigHints("opencode-go", prov, { - id: MUSE_MODEL, - provider: "opencode-go", - }); - expect(hinted.inputModalities).toEqual(["text", "image"]); + for (const m of MUSE_MODELS) { + const hinted = applyProviderConfigHints("opencode-go", prov, { + id: m, + provider: "opencode-go", + }); + expect(hinted.inputModalities).toEqual(["text", "image"]); + } }); });