Compare commits

...
1 Commits
Author SHA1 Message Date
Peter Abbondanzo 65ffc81014 feat: add Muse Spark 1.3 support (#77) 2026-09-06 15:52:03 -07:00
6 changed files with 94 additions and 39 deletions

No files matched your search

+8 -3
View File
@@ -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/<model>` 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
@@ -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/<model>` 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
@@ -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
+29 -20
View File
@@ -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<string, { reasoning?: boolean; options?: Record<string, unknown> }>;
models: Record<
string,
{
reasoning?: boolean;
limit?: { context: number; output: number };
options?: Record<string, unknown>;
}
>;
}
>;
};
@@ -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<string, { models: Record<string, unknown> }>;
};
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)", () => {
+46 -8
View File
@@ -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<string, Record<string, unknown>> = {
"muse-spark-1.2": {
name: "Muse Spark 1.2",
@@ -55,6 +55,36 @@ export const META_MUSE_MODELS: Record<string, Record<string, unknown>> = {
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<string, unknown> = {};
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;
}
+1 -1
View File
@@ -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" }
// } }