diff --git a/README.md b/README.md index a0ed37e..946e019 100644 --- a/README.md +++ b/README.md @@ -763,20 +763,25 @@ fallback key; `ecr doctor` diagnoses setup. } } ``` -- **Meta / Muse Spark 1.2 (public Model API).** Set the default model and any - model-pinned agent frontmatter to `meta/muse-spark-1.2`. ECR supplies the fixed +- **Meta / Muse Spark 1.3 (public Model API).** Set the default model and any + model-pinned agent frontmatter to `meta/muse-spark-1.3`. ECR supplies the fixed public Responses endpoint and the `@ai-sdk/openai` adapter; the generic Chat-Completions compatibility adapter is intentionally not used because it loses Muse's reasoning continuity across tool turns. Standard and Contributor model ids are supported. ```jsonc - "model": "meta/muse-spark-1.2", + "model": "meta/muse-spark-1.3", "auth": { "providers": { "meta": { "mode": "api-key", "tokenEnv": "META_API_KEY" } } } ``` + Standard and Contributor 1.2/1.3 model ids have known capability metadata. Any + future model explicitly named as `meta/` is still routed to the fixed Meta + endpoint with neutral metadata, so a renamed family such as `meta/muse-foo` does + not require an ECR release; Meta validates an unknown id on its first request. + Create the key in the [Meta AI developer portal](https://developer.meta.com/ai/). Locally, export it as `META_API_KEY`. In each review-running workflow, replace the Anthropic credential line with diff --git a/llp/0003-model-runtimes-and-credentials.explainer.md b/llp/0003-model-runtimes-and-credentials.explainer.md index 2082c3e..3363c85 100644 --- a/llp/0003-model-runtimes-and-credentials.explainer.md +++ b/llp/0003-model-runtimes-and-credentials.explainer.md @@ -109,12 +109,15 @@ the Responses API and preserve encrypted reasoning across tool turns. The generi Completions path loses that continuity [observed: `opencode.ts` `META_MODEL_API_BASE_URL` / `META_MUSE_MODELS` / `buildOpencodeConfig`]. -The declared model catalog is an allowlist (`muse-spark-1.2` and -`muse-spark-1.2-contributor`), not a reflection of arbitrary config strings. This keeps -the existing preflight honest: a misspelled or not-yet-supported Meta model remains -absent from the server's provider model list and fails once before any pass. Both models -declare their public context/output limits, multimodal inputs, high reasoning, and -encrypted reasoning inclusion [observed: `opencode.ts` `META_MUSE_MODELS`]. +Provider selection is forward-compatible at the provider boundary, not coupled to the +current Muse family name: every referenced non-empty `meta/` id is declared on +the synthesized Meta provider. Known Muse Spark 1.2 and 1.3 standard/Contributor ids +receive their published limits, multimodal inputs, high reasoning, and encrypted +reasoning inclusion from `META_MUSE_MODELS`; a future id such as `meta/muse-foo` is +declared with neutral metadata rather than guessed capabilities. This deliberately +moves validation of an unknown-but-well-formed Meta id from OpenCode's local preflight +to its first Meta request, in exchange for new Meta names working without an ECR +release [observed: `opencode.ts` `META_MUSE_MODELS` / `buildOpencodeConfig`]. ## Claude Code CLI Containment diff --git a/llp/0009-adoption-templates-and-ci-workflows.guide.md b/llp/0009-adoption-templates-and-ci-workflows.guide.md index cfdb308..0234c2b 100644 --- a/llp/0009-adoption-templates-and-ci-workflows.guide.md +++ b/llp/0009-adoption-templates-and-ci-workflows.guide.md @@ -56,7 +56,7 @@ The three workflows share a posture built entirely around one fact: a reviewer j **Concurrency defaults by auth mode.** The commented `chunk.concurrency` default is 6 for an API key and 3 for a subscription (OAuth) credential, "one account handles many parallel streams poorly, and several PRs may review on the same credential at once" [observed: `templates/config.jsonc:28-30`]. **Muse Spark is opt-in without weakening the default secret surface.** The root config -documents `meta/muse-spark-1.2` plus a `META_API_KEY` auth entry, and each +documents `meta/muse-spark-1.3` plus a `META_API_KEY` auth entry, and each review-running workflow shows the corresponding secret line commented out. It is not forwarded by the default scaffold: adopters replace the Anthropic line and set `ECR_EXPECTED_TOKEN_ENV=META_API_KEY`, so the model process receives only the diff --git a/src/__tests__/opencode.test.ts b/src/__tests__/opencode.test.ts index 114c622..d854285 100644 --- a/src/__tests__/opencode.test.ts +++ b/src/__tests__/opencode.test.ts @@ -342,10 +342,10 @@ test("an upstream-alias auth entry synthesizes a provider block with exactly the expect(opencode.provider?.openai).toBeUndefined(); }); -test("Meta auth synthesizes the public Muse Responses provider without embedding its key", () => { +test("Meta auth synthesizes the public Muse 1.3 Responses provider without embedding its key", () => { const config = configWith({ - agents: [{ id: "correctness", model: "meta/muse-spark-1.2" }], - coordinatorModel: "meta/muse-spark-1.2", + agents: [{ id: "correctness", model: "meta/muse-spark-1.3" }], + coordinatorModel: "meta/muse-spark-1.3-contributor", auth: [{ mode: "api-key", provider: "meta", tokenEnv: "META_API_KEY" }], }); const opencode = buildOpencodeConfig(config) as { @@ -354,7 +354,14 @@ test("Meta auth synthesizes the public Muse Responses provider without embedding { npm: string; options: { baseURL: string; apiKey: string }; - models: Record }>; + models: Record< + string, + { + reasoning?: boolean; + limit?: { context: number; output: number }; + options?: Record; + } + >; } >; }; @@ -365,12 +372,17 @@ test("Meta auth synthesizes the public Muse Responses provider without embedding baseURL: META_MODEL_API_BASE_URL, apiKey: "{env:META_API_KEY}", }); - expect(Object.keys(meta!.models)).toEqual(["muse-spark-1.2"]); - expect(meta!.models["muse-spark-1.2"]?.reasoning).toBe(true); - expect(meta!.models["muse-spark-1.2"]?.options).toMatchObject({ + expect(Object.keys(meta!.models)).toEqual(["muse-spark-1.3", "muse-spark-1.3-contributor"]); + expect(meta!.models["muse-spark-1.3"]?.reasoning).toBe(true); + expect(meta!.models["muse-spark-1.3"]?.limit).toEqual({ + context: 1_048_576, + output: 1_048_576, + }); + expect(meta!.models["muse-spark-1.3"]?.options).toMatchObject({ reasoningEffort: "high", include: ["reasoning.encrypted_content"], }); + expect(meta!.models["muse-spark-1.3-contributor"]?.reasoning).toBe(true); }); test("a Meta-named upstream alias does not redirect the upstream credential to Meta", () => { @@ -400,25 +412,22 @@ test("a Meta-named upstream alias does not redirect the upstream credential to M expect(Object.keys(alias!.models)).toEqual(["muse-spark-1.2"]); }); -test("Meta provider does not register an unknown Muse id, so preflight can reject it", () => { +test("Meta provider registers future model families neutrally and only for the meta prefix", () => { const config = configWith({ - agents: [{ id: "correctness", model: "meta/muse-spark-typo" }], - coordinatorModel: "meta/muse-spark-typo", + agents: [ + { id: "correctness", model: "meta/muse-foo" }, + { id: "security", model: "openai/muse-foo" }, + ], + coordinatorModel: "meta/a-future-family-2", auth: [{ mode: "api-key", provider: "meta", tokenEnv: "META_API_KEY" }], }); const opencode = buildOpencodeConfig(config) as { provider?: Record }>; }; - expect(opencode.provider?.meta?.models).toEqual({}); - expect( - findUnknownModels(["meta/muse-spark-typo"], { meta: ["muse-spark-1.2"] }, ["meta"]), - ).toEqual([ - { - model: "meta/muse-spark-typo", - reason: "model", - suggestions: ["muse-spark-1.2"], - }, - ]); + expect(opencode.provider?.meta?.models).toEqual({ + "muse-foo": {}, + "a-future-family-2": {}, + }); }); test("buildOpencodeConfig registers the no-tools stack verifier (empty tool list)", () => { diff --git a/src/core/opencode.ts b/src/core/opencode.ts index 089cc70..5ace65b 100644 --- a/src/core/opencode.ts +++ b/src/core/opencode.ts @@ -20,10 +20,10 @@ import type { ResearchMcpRuntime } from "./research.js"; export const CLAUDE_CODE_ENGINE = "claude-code" as const; /** Fixed so repo config cannot redirect the Meta credential. */ -// @ref LLP 0003#muse-spark-via-meta-model-api [implements] — fixed endpoint and model allowlist +// @ref LLP 0003#muse-spark-via-meta-model-api [implements] — fixed endpoint, provider routing, and known-model metadata export const META_MODEL_API_BASE_URL = "https://api.meta.ai/v1"; -/** Supported public Muse models. Unknown ids remain visible to preflight. */ +/** Known public Muse models. Other explicit `meta/...` ids use neutral metadata. */ export const META_MUSE_MODELS: Record> = { "muse-spark-1.2": { name: "Muse Spark 1.2", @@ -55,6 +55,36 @@ export const META_MUSE_MODELS: Record> = { include: ["reasoning.encrypted_content"], }, }, + "muse-spark-1.3": { + name: "Muse Spark 1.3", + reasoning: true, + limit: { context: 1_048_576, output: 1_048_576 }, + modalities: { + input: ["text", "image", "pdf", "video"], + output: ["text"], + }, + options: { + store: false, + reasoningEffort: "high", + reasoningSummary: "auto", + include: ["reasoning.encrypted_content"], + }, + }, + "muse-spark-1.3-contributor": { + name: "Muse Spark 1.3 Contributor", + reasoning: true, + limit: { context: 1_048_576, output: 1_048_576 }, + modalities: { + input: ["text", "image", "pdf", "video"], + output: ["text"], + }, + options: { + store: false, + reasoningEffort: "high", + reasoningSummary: "auto", + include: ["reasoning.encrypted_content"], + }, + }, }; /** @@ -265,7 +295,19 @@ export function buildOpencodeConfig( entry.tokenEnv && !entry.upstream ) { - // Muse needs the Responses adapter; openai-compatible uses Chat Completions. + // Meta's Model API uses the Responses adapter; openai-compatible uses Chat + // Completions. Register every explicitly referenced `meta/…` model so a new + // Meta family/name does not require an ECR release merely to select the right + // provider. Known Muse ids get their published capabilities; unknown ids stay + // neutral and are validated by Meta on their first request. + const models: Record = {}; + for (const id of referencedModels) { + const slash = id.indexOf("/"); + if (slash > 0 && id.slice(0, slash) === "meta" && id.length > slash + 1) { + const model = id.slice(slash + 1); + models[model] = META_MUSE_MODELS[model] ?? {}; + } + } provider.meta = { npm: "@ai-sdk/openai", name: "Meta Model API", @@ -273,11 +315,7 @@ export function buildOpencodeConfig( baseURL: META_MODEL_API_BASE_URL, apiKey: `{env:${entry.tokenEnv}}`, }, - models: Object.fromEntries( - Object.entries(META_MUSE_MODELS).filter(([model]) => - referencedModels.includes(`meta/${model}`), - ), - ), + models, }; continue; } diff --git a/templates/config.jsonc b/templates/config.jsonc index f664cf7..364ec57 100644 --- a/templates/config.jsonc +++ b/templates/config.jsonc @@ -87,7 +87,7 @@ // "auth": { "mode": "api-key", "provider": "openai", "tokenEnv": "OPENAI_API_KEY" } // // Muse Spark via the public Meta Model API Responses endpoint: - // set models to `meta/muse-spark-1.2` and configure: + // set models to `meta/muse-spark-1.3` and configure: // "auth": { "providers": { // "meta": { "mode": "api-key", "tokenEnv": "META_API_KEY" } // } }