From 529f3393cbefa0df5db0bea9b491cc9b8f52f9ad Mon Sep 17 00:00:00 2001 From: jun Date: Thu, 3 Sep 2026 11:43:06 +0900 Subject: [PATCH 1/4] docs(devlog): Muse Spark direct-provider roadmap and plan-credential verdict wp0 of 260903_muse_spark_plan_oauth. Docs only; no runtime change. Two questions, two different answers. Can opencodex reach Muse Spark on Meta's own endpoint? Yes, and 010 is a diff-level plan for it: an OpenAI-compatible Responses provider at https://api.meta.ai/v1 built entirely from published spec. No API key was issued; the only contact with the endpoint was an unauthenticated GET that returned 401 invalid_api_key, which proves the host routes /v1 and answers in OpenAI error shape. Can a Muse Code subscription drive it? No, and that answer changed twice. The first draft said no third-party OAuth existed, sourced from a docs search returning 'No matching results' and Authentication's flat 'every request needs an API key'. Installing the CLI disproved it in one command: muse login opens a browser device-approval flow at auth.meta.com/oauth/device that Meta does not document. Finding it did not make it usable. The plan then became: complete a login, extract the credential, fire it at api.meta.ai, ship if it returns 200. The A-gate reviewer named the flaw - that experiment tests whether enforcement is absent, not whether use is permitted, and Meta answered the second question in writing before anyone asked ('This credential is for use with Muse Code only'). An unenforced restriction is still a restriction. So wp2 closes NOOP: no credential extracted, no login completed, no API test, and none exists on this machine. The user-visible half ships in wp1's provider note, which states outright that a Muse Code subscription does not apply here and every call is metered. Four adversarial audit rounds against one independent gpt-5.6-sol reviewer (FAIL, FAIL, FAIL, GO-WITH-FIXES) found what reading alone did not. Two were invisible from the vendor docs and only existed in this repository: - Provider id 'meta' would have hijacked 'meta/muse-spark-1.3', a live Command Code selector merged last week. router.ts:676 resolves the prefix against configured providers, so a working model reference would have silently changed destination. Now 'meta-model', with a routing regression required. - The advertised 'minimal' effort would have been rewritten to 'low' by reasoning-effort.ts:171. The registry array looked correct the whole time, which is why the tests now assert the built Responses body. Also folded: liveModels false (no authenticated roster was ever observed), preserveCustomDestination, decided price overlays with full Cost4, the order-sensitive parity roster, and the stack collapsed to a single PR. --- .../260903_muse_spark_plan_oauth/000_plan.md | 134 +++++++++ .../001_vendor_claim_ledger.md | 141 ++++++++++ .../002_plan_credential_feasibility.md | 116 ++++++++ .../010_wp1_direct_provider.md | 264 ++++++++++++++++++ .../020_wp2_device_oauth.md | 91 ++++++ .../030_wp3_delivery.md | 50 ++++ 6 files changed, 796 insertions(+) create mode 100644 devlog/_plan/260903_muse_spark_plan_oauth/000_plan.md create mode 100644 devlog/_plan/260903_muse_spark_plan_oauth/001_vendor_claim_ledger.md create mode 100644 devlog/_plan/260903_muse_spark_plan_oauth/002_plan_credential_feasibility.md create mode 100644 devlog/_plan/260903_muse_spark_plan_oauth/010_wp1_direct_provider.md create mode 100644 devlog/_plan/260903_muse_spark_plan_oauth/020_wp2_device_oauth.md create mode 100644 devlog/_plan/260903_muse_spark_plan_oauth/030_wp3_delivery.md diff --git a/devlog/_plan/260903_muse_spark_plan_oauth/000_plan.md b/devlog/_plan/260903_muse_spark_plan_oauth/000_plan.md new file mode 100644 index 0000000000..e97780484e --- /dev/null +++ b/devlog/_plan/260903_muse_spark_plan_oauth/000_plan.md @@ -0,0 +1,134 @@ +# Meta Muse Spark: direct Model API provider + plan-credential question + +- Date: 2026-09-03 +- Session: `01a064b2-91b5-7272-b9ef-4db66bb46921` +- Work class: C3 — a new provider registry entry, a published-spec effort ladder, catalog metadata, and a plan-credential feasibility verdict. +- Status: A (wp0, docs-only roadmap cycle; three audit rounds folded). + +## Loop spec + +- Archetype: satisfy-spec integration, with one genuinely open question (wp2) that resolved to a recorded negative rather than code. +- Trigger: the user asked whether Meta's Muse Spark *plan* can be attached, after `878f75417` landed Muse Spark 1.3 through the Command Code and OpenCode Zen resellers. +- Goal: reach Muse Spark **directly** on Meta's own endpoint, and settle — with evidence, not inference — whether a Muse Code subscription can legitimately drive a local proxy. +- Non-goals: issuing a Meta API key, entering payment details, touching generated metadata, changing any `*-free` Zen id, retiring or redefaulting any model, altering the merged 1.3 work. +- Verifier: focused `bun test ` on touched suites plus `bun x tsc --noEmit`. **The repository-wide local suite is forbidden by standing user instruction**; exact-head GitHub CI is the authoritative gate. +- Stop condition: every work-phase closed and the single implementation PR green on its exact head SHA and merged into `dev`. +- Memory artifact: this unit folder. +- Terminal outcomes: wp1 targets `DONE`; wp2 closed `NOOP` on a licence finding. `BLOCKED` remains available if CI or branch protection refuses for an unrelated reason. +- Escalation: each A gate dispatches one independent read-only reviewer on `gpt-5.6-sol` at high effort. Two failed correction loops on the same packet stops the phase and reports. + +## Revision after the A-gate audit (round 1: FAIL, 8 blockers) + +An independent `gpt-5.6-sol` reviewer failed the first draft, and a third-party user +report arrived in the same window. Between them, the plan changed shape: + +| Was | Now | Why | +|---|---|---| +| provider id `meta` | `meta-model` | `meta/muse-spark-1.3` is a LIVE Command Code selector; `router.ts:676` would have hijacked it, and `init.ts:72` would have derived `META_API_KEY` — the CLI's variable, not the API's | +| `liveModels: true` | `false` | no authenticated `/v1/models` payload was ever seen; Meta serves image and voice families on the same base URL | +| effort array only | plus identity `modelReasoningEffortMap` | `reasoning-effort.ts:171` rewrites `minimal` to `low`; the array assertion passed while the wire was wrong | +| "no OAuth exists" | a device-code-shaped login exists | `muse login` opens `auth.meta.com/oauth/device`; the docs simply do not mention it. Finding it did not make it usable — see below | +| 2-layer stack | 1 PR | the disclosure folds into wp1, and wp2 ships no code | +| wp3 as a work-phase | delivery ceremony inside each phase | delivery is not independently implementable | + +**The correction worth naming.** `001` §G concluded no third-party OAuth flow existed, +from a docs-site search returning *No matching results* and Authentication's flat "every +request needs an API key". Both readings were accurate; the inference was not. Installing +the CLI and running `muse login --help` disproved it in one command. Absence from a +vendor's docs is not absence from the product — and the reviewer catching the adjacent +SDK claim is what sent me to check. + +## The decision this plan turns on + +A reseller path already works. `command-code/meta-muse-spark-1.3` and `opencode-go/muse-spark-1.3-contributor` shipped in #3317, so nothing here is about *reaching* the model. What is missing is the direct route and, more importantly, an answer to the question the user actually asked. + +**The plan credential is scoped out by the vendor, in writing.** `dev.meta.ai/docs/muse-code/subscriptions` states it twice: + +> The subscription applies to the Muse Code API key that is automatically connected in the Muse Code CLI onboarding process. **This credential is for use with Muse Code only.** Any additional API keys you create under your Meta Model API account will be billed through pay-as-you-go. + +> Your subscription **only works through the Muse Code CLI** while signed in with your Meta Model API account. + +That is a licence boundary, not a technical one — and it survives the OAuth discovery +intact. The two questions are now cleanly separable: + +- **Mechanism:** could opencodex hold this credential? A device-code-shaped login + exists, so possibly. **Not measured, deliberately** — see below. +- **Entitlement:** may it be spent outside Muse Code? The vendor has answered no. + +Only the second question decides whether anything ships, and it is already answered. So +the mechanism was left unmeasured rather than tested: an experiment that can only +discover whether enforcement is absent cannot produce a result that licenses shipping. +wp2 closed `NOOP` on that basis (`020`). + +A third-party user report (Threads, 2026-09-03) claims pay-as-you-go bills through by +default under the plan, and that the endpoints are not separated. Both are **unverified** +and neither changes the outcome — the second is precisely the enforcement-absence +observation above. + +The user-visible consequence lands in wp1 regardless: the provider note says outright +that a Muse Code subscription does not apply and every call is metered. + +## The second decision: wire shape + +Meta publishes an OpenAI-compatible surface at `https://api.meta.ai/v1` carrying both `POST /v1/responses` and `POST /v1/chat/completions`, and the quickstart hands the OpenAI SDK that exact `base_url`. Responses is the documented recommendation for agentic work ("the recommended default for new work"), and it is the surface that carries `input_image` and reasoning replay. + +So the provider is `adapter: "openai-responses"`, not `openai-chat`. Registering it as a Chat provider would work but would forfeit the reasoning-replay and native-multimodal path the vendor recommends, and it would diverge from how `openai-apikey` is already registered against the same wire. + +## Work-phase map (dependency-ordered, PHASE-SPLIT-01) + +| Phase | Doc | Delivers | PR | +|---|---|---|---| +| wp0 | this folder + `001`, `002` | claim ledger, feasibility research, diff-level decade docs | — | +| wp1 | `010_wp1_direct_provider.md` | `meta-model` key provider, ladder + wire map, parity/pricing/docs updates, behavior tests | PR 1, base `dev` | +| wp2 | `020_wp2_device_oauth.md` | **CLOSED, `NOOP`** — recorded negative, no code | none | + +**One PR, no stack (`DEV-STACK-01`).** The first draft stacked wp2 on wp1 over a string +wp1 introduced; that string is folded into wp1, and wp2 now ships no code at all. Round +2 also caught that a "second independent PR" would still have consumed wp1's +module-private constants — so there was never a clean independence claim to make. +`030` is delivery procedure, not a work-phase. + +## Why wp2 closed instead of shipping + +A real `muse login` **does** open a browser device-approval flow — the docs simply never +mention it, and the first draft of `001` §G wrongly concluded no such flow existed. + +Finding it did not make it usable. The round-2 audit put it plainly: proving a credential +is technically reusable is not the same as being allowed to reuse it. The experiment I +had planned — extract the credential, fire it at `api.meta.ai`, ship if it returns 200 — +tested whether **enforcement was absent**, not whether **use was permitted**. Meta +answered the second question in writing before anyone asked: "This credential is for use +with Muse Code only." + +So no credential was extracted, no login was completed, and a targeted check confirms +none exists on this machine. The finding ships as `020` plus the user-facing disclosure +in wp1's note. + +## Scope + +### IN + +- `src/providers/registry.ts` — one new entry plus its effort/window/modality constants and wire map +- `tests/provider-registry-parity.test.ts` — the hardcoded key-provider roster +- `src/usage/expected-prices.ts` + `tests/usage-cost.test.ts` — two price rows and the pinned count (64 → 66) +- `docs-site/` English provider table (`src/AGENTS.md:29` requires it) +- `tests/` — a focused suite beside the existing provider tests +- `src/oauth/` — **nothing.** wp2 closed as a negative; no OAuth code ships. +- `devlog/_plan/260903_muse_spark_plan_oauth/` + +### OUT + +- `muse serve` / the MSP SDK — a stdio JSON-RPC **agent session** host, not a model endpoint. Bridging it would mean re-hosting an agent runtime inside a proxy and discarding the part that makes it an agent (`002`). +- Translated `docs-site` locales — English source only. +- `src/generated/model-metadata.ts`, `scripts/model-metadata.source.json` — generated from a vendor snapshot; hand-editing them is forbidden by the repo's own convention. +- (`src/usage/expected-prices.ts` moved to IN. `src/usage/cost.ts:267` resolves a generated-metadata alias first, and `meta-model` has none, so an unpriced row falls through and reports no cost at all. Two overlays are required, not optional.) +- Muse Voice Transcribe (`wss://api.meta.ai/v1/asr/realtime`, `POST /v1/asr/transcribe`) — a different transport, out of scope. + +## Accept criteria (goalplan c-1 through c-6) + +1. `c1` — this unit carries 000-range research plus a diff-level decade doc per implementation phase. +2. `c2` — every registry fact traces to a published vendor statement in `001`. +3. `c3` — no API key is issued and no billing detail is entered. +4. `c4` — the plan-credential question is answered by working wiring or a recorded negative with the blocking evidence. **Met by `020`'s negative.** +5. `c5` — `tsc` exits 0, focused tests pass, the full local suite is never run. +6. `c6` — the implementation PR green at its exact head SHA and merged into `dev`. diff --git a/devlog/_plan/260903_muse_spark_plan_oauth/001_vendor_claim_ledger.md b/devlog/_plan/260903_muse_spark_plan_oauth/001_vendor_claim_ledger.md new file mode 100644 index 0000000000..5f36e67e39 --- /dev/null +++ b/devlog/_plan/260903_muse_spark_plan_oauth/001_vendor_claim_ledger.md @@ -0,0 +1,141 @@ +# Vendor claim ledger — Meta Model API and Muse Code + +Every row is a statement Meta publishes, retrieved 2026-09-03 through a signed-in +browser (Aside CLI `1.26.902.1732`, account u0) because `dev.meta.ai/docs` returns +HTTP 500 to a plain fetch and its `.md` exports 500 as well. Rendered DOM was the +only readable surface. + +Nothing here is inferred. A fact the vendor does not state is written `NOT STATED` +and does not reach the registry. + +## A. Transport + +| Claim | Value | Source | +|---|---|---| +| Base URL | `https://api.meta.ai/v1` | `/docs/quickstart`, `/docs/coding-agents` | +| Responses endpoint | `POST /v1/responses` | `/docs/protocols` | +| Chat endpoint | `POST /v1/chat/completions` | `/docs/protocols` | +| OpenAI compatibility | "It is OpenAI-compatible and exposes the full feature set" (Responses) | `/docs/protocols` | +| SDK base_url, verbatim | `base_url="https://api.meta.ai/v1"` / `baseURL: 'https://api.meta.ai/v1'` | `/docs/quickstart` | +| Auth header | `Authorization: Bearer $MODEL_API_KEY` | `/docs/api-reference`, `/docs/authentication` | +| Env var | `MODEL_API_KEY` (the CLI's own var is the different `META_API_KEY`) | `/docs/authentication`, `/docs/muse-code/auth` | +| Recommended surface | Responses is "the recommended default for new work" | `/docs/protocols` | + +**Independent liveness check, no key issued.** `GET https://api.meta.ai/v1/models` +returned `401 {"error":{"code":"invalid_api_key","message":"Unauthorized",...}}`. +That is worth more than a docs quote: it proves the host exists, terminates TLS, +routes `/v1`, and answers in OpenAI error shape — while confirming we hold no +credential. This is the whole of our contact with the endpoint. + +## B. Model facts + +| Claim | `muse-spark-1.3` | `muse-spark-1.3-contributor` | Source | +|---|---|---|---| +| Model id, verbatim | `muse-spark-1.3` | `muse-spark-1.3-contributor` | `/docs/models` | +| Context window | 1,048,576 | 1,048,576 | `/docs/models` | +| Max output tokens | NOT STATED | NOT STATED | see below | +| Input modalities | text, image, video, audio\*, PDF | same | `/docs/models` | +| Output | text only | text only | `/docs/models` | +| Input price /1M | $1.25 | $0.10 | `/docs/pricing-rate-limits` | +| Cached input /1M | $0.15 | $0.002 | `/docs/pricing-rate-limits` | +| Output price /1M | $4.25 | $0.20 | `/docs/pricing-rate-limits` | + +\* Audio on 1.3 is documented as "not fully supported" with degraded quality. + +**Max output tokens is genuinely unpublished, and the number that looks like an +answer is a trap.** `131072` appears in the docs only inside a third-party +`opencode.json` sample; a docs search for the literal returns *No matching results*. +The protocol pages say `max_completion_tokens` is "Model-dependent" and that +exceeding the model's configured maximum returns HTTP 400. So the registry declares +no `defaultMaxOutputTokens` for these models rather than promoting a sample value +into a capability claim. + +**Price cross-check.** These are the same numbers the Command Code models payload +carries for `meta/muse-spark-1.3` (1.25 / 4.25) and `meta/muse-spark-1.3-contributor` +(0.1 / 0.2), read independently on 2026-09-03. The reseller republishes Meta's list +price, which corroborates both readings. + +## C. Reasoning effort + +> Accepted values: "none", "minimal", "low", "medium", "high", "xhigh". When omitted, +> the model reasons by default. "none" (disable reasoning) is not supported by Muse +> Spark and returns HTTP 400. — `/docs/reasoning` + +Two consequences for the registry, and the second is the one that bites: + +- The usable ladder is `minimal, low, medium, high, xhigh`. `none` is published as an + API-wide value and separately excluded for this model family, so advertising it + would hand the user a picker entry that 400s. +- `max` and `ultra` are **not** in the vendor's set. Several opencodex ladders end in + `max` and it would be easy to append one by family resemblance; here that would + invent a wire value. + +Independent corroboration from the sibling gateway: an unauthenticated Zen probe of +`muse-spark-1.3-contributor-free` on 2026-09-03 accepted `minimal|low|medium|high|xhigh` +and rejected `max` and `ultra` with `unknown variant`, and rejected `none` with +"does not support none with this model". Two independent surfaces, same ladder. + +## D. Image input + +| Surface | Content-part type | Source | +|---|---|---| +| Responses | `input_image`, `image_url` a plain string | `/docs/image-understanding` | +| Chat Completions | `image_url` wrapping `{ url }` | `/docs/image-understanding` | + +Up to 50 images per request; more returns HTTP 400. Images only in user-role messages. + +## E. Muse Code subscription — the licence boundary + +| Tier | Price | Source | +|---|---|---| +| Everyday Usage | $5.00/mo | `/ai/products/muse-code/`, `/help/subscriptions/what-is-a-muse-code-subscription` | +| High Usage | $15.00/mo | same | +| Power Usage | $50.00/mo | same | + +> The subscription applies to the Muse Code API key that is automatically connected in +> the Muse Code CLI onboarding process. **This credential is for use with Muse Code +> only.** Any additional API keys you create under your Meta Model API account will be +> billed through pay-as-you-go. — `/docs/muse-code/subscriptions` + +> Your subscription **only works through the Muse Code CLI** while signed in with your +> Meta Model API account. — same page + +## F. CLI + +- Install: `curl -fsSL https://dev.meta.ai/install.sh | sh` — `/docs/muse-code/` +- The installer fetches a launcher from `https://api.meta.ai/muse-launcher.sh` + (`MUSE_LAUNCHER_URL`), installs to `${MUSE_INSTALL_DIR:-~/.local/bin}/muse`, and + verifies a sha256. Read directly from the retrieved script, HTTP 200, 9314 bytes. +- Auth precedence: `META_API_KEY` env, then a stored key, then a stored browser + session. "An API key always takes priority over a browser sign-in." — `/docs/muse-code/auth` +- The **docs** describe no dedicated login command — first run prompts, `/login` + re-opens, `muse auth set` stores a key, `muse logout` signs out. The installed CLI + does ship `muse login`, which the docs omit; that gap and what it does (and does not) + prove are recorded in `002`. + +## G. Third-party OAuth + +**NOT STATED — and searched for, not merely unseen.** The docs site search returns +*No matching results for "OAuth"*. Authentication states "Every request to Meta Model +API needs an API key". No device-code, PKCE, or authorization-code flow appears under +Authentication, API reference, SDKs, coding agents, or agent frameworks. The only +browser sign-in documented belongs to the Muse Code CLI and its wire protocol is not +published. + +## H. Account and payment + +Signup is email + confirmation with no card at account creation, but adding a payment +method is a listed prerequisite "to start making requests", alongside creating an API +key (`/help/accounts-and-login/sign-up`, `/docs/muse-code/auth`). Eligibility: 18+, +supported country, team-owner signup. + +**No account was created, no key issued, no payment method entered.** + +## Provenance caveat + +`/docs/pricing-rate-limits` carries an unremoved internal editorial note asking someone +to "confirm these rate-limit numbers against the launch configuration before +publishing". That caveat attaches to the **rate-limit** figures (Standard 3,000 RPM / +4M TPM; Contributor 100 RPM / 3M TPM), which is exactly why no RPM/TPM value is wired +into the registry. The per-token prices are corroborated by the Command Code payload +and are not affected. diff --git a/devlog/_plan/260903_muse_spark_plan_oauth/002_plan_credential_feasibility.md b/devlog/_plan/260903_muse_spark_plan_oauth/002_plan_credential_feasibility.md new file mode 100644 index 0000000000..586ea84a38 --- /dev/null +++ b/devlog/_plan/260903_muse_spark_plan_oauth/002_plan_credential_feasibility.md @@ -0,0 +1,116 @@ +# Plan-credential feasibility — research + +Research doc. No diffs here (LEXICO-SPLIT-01); the implementation shape lives in the +decade docs. + +> **Outcome: this research closed wp2 as a `NOOP` negative. See `020`.** No credential +> was extracted, no login completed, and no Meta credential exists on this machine. + +## The docs were not the whole truth + +`001` §G recorded that no third-party OAuth flow is published, sourced from a docs-site +search returning *No matching results for "OAuth"* and from Authentication's flat +"Every request to Meta Model API needs an API key". Both readings were accurate. + +**The conclusion drawn from them was wrong.** A browser-approval login does exist; Meta +just does not document it. Measured on 2026-09-03 by installing the CLI and running it: + +``` +$ muse login +Open this page to sign in: + https://auth.meta.com/oauth/device/?code= +confirm this code matches: + + +Waiting for approval… +``` + +That is **device-code-shaped**: a user code approved in a browser against +`auth.meta.com/oauth/device`. It is deliberately not called RFC 8628 here. A user-code +URL does not establish the token endpoint, scopes, rotation, expiry semantics, or — the +part that actually matters — that any client other than Muse Code may hold the result. +`muse login --help` says only: "Log in with your Meta account: approve a code in your +browser. META_API_KEY always takes priority over the account login." + +The lesson worth keeping: **absence from a vendor's docs is not absence from the +product.** A docs search proved what Meta publishes, and I let it stand for what Meta +implements. One `--help` disproved it. The opposite error was available too, and the +A-gate caught it: finding an undocumented flow is not the same as being allowed to use +it. + +## What the CLI actually is + +`muse --version` → `Muse Code 1.0.2 (1.0.2-R2040.1)`, installed to `~/.local/bin/muse` +by `https://dev.meta.ai/install.sh` (which fetches a launcher from +`https://api.meta.ai/muse-launcher.sh` and verifies a sha256). + +Subcommands relevant here: `login`, `logout`, `auth set --api-key-stdin`, `serve`, +`exec`, `schema`. + +Its own reasoning ladder, from `muse --help`: + +> `--reasoning-effort ` Meta reasoning effort: none|minimal|low|medium|high|xhigh|ultra (default: high) + +Note `ultra`, which the public `/docs/reasoning` page does not list. Another instance of +the same gap. The registry ladder in `010` stays with the twice-corroborated +`minimal..xhigh` set, because `ultra` here is a CLI flag rather than a proven Model API +wire value, and Zen's probe rejected it. + +## `muse serve` is not an OpenAI-compatible endpoint + +The A-gate reviewer raised the published SDK +([meta-models/muse-code-sdk](https://github.com/meta-models/muse-code-sdk), HTTP 200) as +a route the categorical negative overlooked. It is a real route, and it is not the route +we want. + +`muse serve --help`: "serve an MSP session host over **stdio**. The client owns this +process's stdin and stdout and is its only connection." + +MSP is a JSON-RPC **agent session** protocol — `session/start`, `turn/start`, +`approval/decide`, `item/delta`, `subagent/*`, `view/page`. It owns the tool loop, +approvals, sandbox posture, and session durability. opencodex is a **model proxy**: it +forwards Responses/Chat requests and returns completions. Bridging MSP to +`/v1/responses` would mean re-hosting an entire agent runtime inside the proxy and +then discarding the half that makes it an agent. + +So the SDK is correctly out of scope — but for an architectural reason, not the licence +reason `020` originally gave. The reviewer was right that the stated ground was wrong. + +## The three routes, ranked + +| Route | Mechanism | Status | +|---|---|---| +| Direct API key | `MODEL_API_KEY` on `https://api.meta.ai/v1` | Implementable now, spec-only. **wp1.** | +| Device-code-shaped login | `auth.meta.com/oauth/device`, as `muse login` uses | Exists but undocumented; **wp2 closed `NOOP`** — the credential is licensed to Muse Code only. | +| MSP host bridge | `muse serve` over stdio | Out of scope: wrong protocol class. | + +## Why the investigation stopped here + +No login was ever approved. Both attempts were terminated with the grant pending, and a +targeted check for a Muse credential on this machine found none. + +The original next step was to complete a login and measure where the credential lands, +what it is, and whether it authenticates `https://api.meta.ai/v1`. That plan was +abandoned on review, and the reason is worth stating plainly: **it was a test for +whether enforcement is absent, not for whether use is permitted.** Meta answered the +second question in writing before anyone asked (`001` §E). Discovering that a +restriction is unenforced does not lift it, so completing the measurement could not have +produced a result that licensed shipping. + +A third-party report (Threads, 2026-09-03) claims the stored key is plaintext in the +macOS Keychain and that the endpoints are not separated. Both remain **unverified**, and +neither changes the outcome: the second, if true, is precisely the enforcement-absence +observation above. + +## The licence question is the whole answer + +`/docs/muse-code/subscriptions` says the subscription credential is "for use with Muse +Code only", and a credential obtained by `muse login` is that credential. Mechanism and +entitlement are separable questions, and only entitlement decides whether anything +ships. The vendor has answered it. + +`src/oauth/index.ts` already carries the adjacent precedent on Anthropic — +`defaultRefreshPolicy: "disabled"`, with a comment recording that the vendor +server-side-blocks subscription OAuth outside its own clients. That posture mitigates a +risk on a flow that already exists; it does not authorize creating a new one against a +published prohibition. diff --git a/devlog/_plan/260903_muse_spark_plan_oauth/010_wp1_direct_provider.md b/devlog/_plan/260903_muse_spark_plan_oauth/010_wp1_direct_provider.md new file mode 100644 index 0000000000..24285c3f08 --- /dev/null +++ b/devlog/_plan/260903_muse_spark_plan_oauth/010_wp1_direct_provider.md @@ -0,0 +1,264 @@ +# wp1 — direct Meta Model API provider + +Single PR. Base: `dev`. Branch: `codex/meta-model-api-provider`. + +Every value below is a `001` ledger row. Nothing is added by resemblance to a +neighbouring provider. + +**Revised after the A-gate audit (round 1, FAIL, 8 blockers).** Six changes below carry +an audit provenance note. The two that mattered most were invisible from the docs and +only showed up in the repository: the provider id would have hijacked an existing model +namespace, and the advertised `minimal` effort would have been silently rewritten to +`low` on the wire. + +## MODIFY `src/providers/registry.ts` + +### 1. Constants, beside the other provider ladders (near `OPENAI_API_GPT56_REASONING_EFFORTS`, line ~437) + +```ts +/* + * Meta Model API (https://api.meta.ai/v1). Published ladder, NOT the usual house set: + * /docs/reasoning lists "none", "minimal", "low", "medium", "high", "xhigh" and then + * excludes "none" for Muse Spark specifically ("not supported by Muse Spark and + * returns HTTP 400"). "max" and "ultra" are absent from the vendor's list entirely, + * so appending one by family resemblance would invent a wire value. + * + * Corroborated against a second surface: an unauthenticated OpenCode Zen probe of + * muse-spark-1.3-contributor-free on 2026-09-03 accepted minimal..xhigh and rejected + * max/ultra with \`unknown variant\`, and rejected none with "does not support none + * with this model". + */ +const META_MUSE_REASONING_EFFORTS = ["minimal", "low", "medium", "high", "xhigh"]; + +/* + * Identity wire map (audit blocker 3). `requestToCodexEffort` in + * src/reasoning-effort.ts:171 rewrites `minimal` to `low` unless a model-scoped wire + * map says otherwise. Without this the picker would advertise an effort the wire never + * sends, and a registry-array assertion would happily pass while the request body was + * wrong. The map is identity because Meta's values ARE the Codex names. + */ +const META_MUSE_REASONING_EFFORT_MAP: Record = Object.fromEntries( + META_MUSE_REASONING_EFFORTS.map(effort => [effort, effort]), +); + +/** Muse Spark 1.3 and its Contributor tier both publish a 1,048,576-token window (/docs/models). */ +const META_MUSE_CONTEXT_WINDOW = 1_048_576; + +const META_MUSE_MODELS = ["muse-spark-1.3", "muse-spark-1.3-contributor"]; +``` + +### 2. The provider entry, after the `openai-apikey` entry (line ~1450) + +**Id is `meta-model`, not `meta` (audit blocker 1).** Two independent collisions, both +verified in the tree: + +- `src/router.ts:676` resolves a `/` prefix against configured + providers first. Registering `meta` would make the existing Command Code native + selector `meta/muse-spark-1.3` — already live on `dev` since #3317 — silently change + destination the moment a user configured the direct provider. A working model + reference would start billing somewhere else, with no error. +- `src/cli/init.ts:72` derives the env var as `${ID.toUpperCase()}_API_KEY`, so id + `meta` yields `META_API_KEY` — which is the **Muse Code CLI's** variable, not the + Model API's `MODEL_API_KEY`. Two different credentials under one name. + +`meta-model` derives `META_MODEL_API_KEY` and collides with neither. + +```ts + { + id: "meta-model", + label: "Meta Model API", + adapter: "openai-responses", + baseUrl: "https://api.meta.ai/v1", + authKind: "key", + featured: false, + dashboardUrl: "https://dev.meta.ai/docs/authentication", + defaultModel: "muse-spark-1.3", + models: META_MUSE_MODELS, + /* + * Static roster (audit blocker 4). Meta serves several families on this base URL — + * Muse Image, Muse Voice Transcribe (wss://.../asr/realtime) — and we hold no key, + * so no authenticated /v1/models payload was ever observed. `liveModels: true` + * would publish that unseen roster into the picker, including models this + * Responses-agent provider cannot drive. Seed the two ids the vendor documents; + * revisit with a real payload fixture. + */ + liveModels: false, + /* + * Audit blocker 2. A user may already own a custom provider named `meta-model` + * pointing somewhere else; without this, registry.ts:2995 canonicalizes its + * adapter and base URL and their saved key gets sent to Meta. registry.ts:147 + * names this the required protection for a newly promoted id. + */ + preserveCustomDestination: true, + /* + * Responses, not Chat. Meta publishes both POST /v1/responses and + * POST /v1/chat/completions at the same base URL and calls Responses "the + * recommended default for new work ... OpenAI-compatible and exposes the full + * feature set", including reasoning replay across tool turns and native + * input_image parts. Registering this as openai-chat would reach the model and + * silently forfeit both. + */ + modelContextWindows: Object.fromEntries(META_MUSE_MODELS.map(id => [id, META_MUSE_CONTEXT_WINDOW])), + modelInputModalities: Object.fromEntries(META_MUSE_MODELS.map(id => [id, ["text", "image"]])), + modelReasoningEfforts: Object.fromEntries(META_MUSE_MODELS.map(id => [id, META_MUSE_REASONING_EFFORTS])), + modelReasoningEffortMap: Object.fromEntries(META_MUSE_MODELS.map(id => [id, META_MUSE_REASONING_EFFORT_MAP])), + /* + * The disclosure is folded in here rather than shipped as a second stacked PR + * (audit blocker 7): it is one string on this same entry, so a separate layer buys + * a second CI and review cycle and no reviewability. + */ + note: "Pay-as-you-go Meta Model API. Get a key at https://dev.meta.ai — a Meta developer account needs a payment method before it can serve requests, and every call is metered per token. A Muse Code subscription does NOT apply here: Meta scopes that credential to the Muse Code CLI and bills any other key pay-as-you-go (dev.meta.ai/docs/muse-code/subscriptions). The Contributor tier (muse-spark-1.3-contributor) is ~92% cheaper because Meta trains on your prompts; do not send confidential material through it. Muse Spark is also reachable through the command-code and opencode-go providers.", + }, +``` + +Three deliberate omissions, each one a fact the vendor does not publish: + +- **No `defaultMaxOutputTokens`.** `001` §B: the only number available (`131072`) + lives inside a third-party config sample and the docs call the real limit + "model-dependent". Declaring it would be a capability claim we cannot source. +- **No video/audio/PDF in `modelInputModalities`.** The catalog enum is + `text`/`image` — `tests/catalog-input-modality-enum.test.ts` exists precisely + because a provider once advertised `video` and poisoned the exported config. + Audio is documented as degraded on 1.3 anyway. +- **No rate-limit metadata.** The pricing page carries an unremoved internal note + asking someone to confirm those numbers pre-launch (`001` provenance caveat). +- **No `oauthId`.** A device-code-shaped login does exist (`002`), but the credential it + yields is licensed to the Muse Code CLI alone, so wp2 closed `NOOP` and no OAuth is + wired (`020`). This entry is key-auth only. + +## MODIFY `tests/provider-registry-parity.test.ts` + +`EXPECTED_KEY_PROVIDER_IDS` at line 33 is a hardcoded roster and the assertion compares +**order**, not set membership. Insert `"meta-model"` immediately after +`"openai-apikey"`, matching where the entry sits in the registry — appending it to the +end fails (audit round 3, blocker 1). + +## MODIFY `src/usage/expected-prices.ts` and `tests/usage-cost.test.ts` + +Decided, not deferred (audit round 2, blocker 4). `src/usage/cost.ts:267` resolves a +generated-metadata alias first and `meta-model` has none, so an unpriced row falls all +the way through and reports nothing. Two overlays, values from `001` §B and +corroborated by the Command Code payload: + +Complete `ExpectedPriceOverlay` objects — `source`, `verifiedAt`, and `status` are +required, and the earlier draft's trailing `...` would not compile (audit round 3, +blocker 2): + +```ts +const META_MODEL_PRICING = "https://dev.meta.ai/docs/pricing-rate-limits"; + + { provider: "meta-model", modelId: "muse-spark-1.3", + cost4: { input: 1.25, output: 4.25, cacheRead: 0.15, cacheWrite: 0 }, + source: `Meta Model API published price ${META_MODEL_PRICING}; cached input billed at 0.12x base input`, + verifiedAt: "2026-09-03", status: "verified" }, + { provider: "meta-model", modelId: "muse-spark-1.3-contributor", + cost4: { input: 0.10, output: 0.20, cacheRead: 0.002, cacheWrite: 0 }, + source: `Meta Model API published Contributor-tier price ${META_MODEL_PRICING}; data-sharing discount tier`, + verifiedAt: "2026-09-03", status: "verified" }, +``` + +`status: "verified"` rather than `"verified-derived"`: these are Meta's own list prices +for Meta's own endpoint, read from the vendor page and independently corroborated by the +Command Code payload — no cross-surface inference is involved. + +`cacheWrite` is `0` because Meta publishes no cache-write charge, the same shape +`GEMINI_31_PRO` already uses. + +`tests/usage-cost.test.ts:300` pins the overlay count at 64 — update to 66 in the same +commit and add exact-lookup assertions for both ids. + +## NEW `tests/meta-model-api-provider.test.ts` + +Six assertions, each pinning a ledger row that a future edit could silently break: + +```ts +import { describe, expect, test } from "bun:test"; +import { getProviderRegistryEntry } from "../src/providers/registry"; +import { providerConfigSeed } from "../src/providers/derive"; + +describe("Meta Model API provider (meta-model)", () => { + test("routes to the published OpenAI-compatible Responses base URL", () => { + const entry = getProviderRegistryEntry("meta-model"); + expect(entry?.baseUrl).toBe("https://api.meta.ai/v1"); + expect(entry?.adapter).toBe("openai-responses"); + expect(entry?.authKind).toBe("key"); + }); + + test("advertises exactly the vendor's effort ladder", () => { + const entry = getProviderRegistryEntry("meta-model"); + for (const id of ["muse-spark-1.3", "muse-spark-1.3-contributor"]) { + expect(entry?.modelReasoningEfforts?.[id]).toEqual(["minimal", "low", "medium", "high", "xhigh"]); + } + }); + + test("never advertises an effort the vendor rejects", () => { + const entry = getProviderRegistryEntry("meta-model"); + const efforts = entry?.modelReasoningEfforts?.["muse-spark-1.3"] ?? []; + // none -> HTTP 400 on Muse Spark; max/ultra are not in the published set at all. + for (const forbidden of ["none", "max", "ultra"]) expect(efforts).not.toContain(forbidden); + }); + + test("declares the published 1M window for both tiers", () => { + const entry = getProviderRegistryEntry("meta-model"); + for (const id of ["muse-spark-1.3", "muse-spark-1.3-contributor"]) { + expect(entry?.modelContextWindows?.[id]).toBe(1_048_576); + } + }); + + test("advertises no modality outside the catalog enum", () => { + const entry = getProviderRegistryEntry("meta-model"); + for (const id of ["muse-spark-1.3", "muse-spark-1.3-contributor"]) { + expect(entry?.modelInputModalities?.[id]).toEqual(["text", "image"]); + } + }); + + test("claims no max-output limit, because the vendor publishes none", () => { + const entry = getProviderRegistryEntry("meta-model"); + expect(entry?.defaultMaxOutputTokens).toBeUndefined(); + }); + + test("the seed survives derive() intact", () => { + const entry = getProviderRegistryEntry("meta-model")!; + const seed = providerConfigSeed(entry); + expect(seed.baseUrl).toBe("https://api.meta.ai/v1"); + expect(seed.modelContextWindows?.["muse-spark-1.3"]).toBe(1_048_576); + }); +}); +``` + +## Behavior-level tests the audit demanded (blockers 1, 3, 6) + +Registry-shape assertions alone would have passed against all three defects. Add: + +**Every registry lookup in this suite uses `getProviderRegistryEntry("meta-model")`.** +The id changed after the first draft; a stale `"meta"` returns `undefined` and the +non-null seed lookup throws (audit round 2, blocker 3). Required cases: + +| Case | Asserts | Why a registry-shape check is not enough | +|---|---|---| +| namespace | `routeModel(cfg, "meta/muse-spark-1.3").providerName === "command-code"` with BOTH providers configured | the live Command Code selector must survive; note the field is `providerName`, not `provider` (`src/router.ts:61`) | +| wire effort | built Responses body has `reasoning.effort === "minimal"` | the registry array looked right while `reasoning-effort.ts:171` rewrote it | +| destination | a same-named custom provider keeps its base URL, adapter, and key | `preserveCustomDestination` | +| roster | `liveModels === false` | an unseen authenticated roster must not reach the picker | +| disclosure | note contains the subscription and training warnings | folding it into wp1 must not lose its regression (audit round 2, blocker 3) | +| transport | `baseUrl`, `adapter`, `authKind`, ladder, window, modalities, absent `defaultMaxOutputTokens` | ledger rows | + +B writes these against the real helpers — `routeModel` and the Responses adapter's +`buildRequest` — with real fixtures. + +## Documentation (audit blocker 6) + +`src/AGENTS.md:29` requires user-facing configuration changes to reach `docs-site/`. +A new provider is one. B adds the row to the English provider table only; translated +locales are left alone rather than machine-guessed. + +## Verification + +`bun test tests/meta-model-api-provider.test.ts tests/provider-registry-parity.test.ts tests/usage-cost.test.ts` +— all three unconditionally, since both overlays are now mandatory — then +`bun run test:changed` (`src/AGENTS.md:26` requires it once the touch set is broader +than one file; it is import-graph-scoped, not the forbidden repository-wide suite), then +`bun x tsc --noEmit` and `bun run privacy:scan` (this change ships credential guidance). + +Branch from the current `origin/dev` tip, not from a remembered SHA: `dev` moved during +the audit rounds. diff --git a/devlog/_plan/260903_muse_spark_plan_oauth/020_wp2_device_oauth.md b/devlog/_plan/260903_muse_spark_plan_oauth/020_wp2_device_oauth.md new file mode 100644 index 0000000000..311b50069d --- /dev/null +++ b/devlog/_plan/260903_muse_spark_plan_oauth/020_wp2_device_oauth.md @@ -0,0 +1,91 @@ +# wp2 — Muse Code plan credential: CLOSED as a recorded negative + +**Outcome: `NOOP`. No code ships. No PR is opened.** + +This phase existed to answer whether a Muse Code subscription can drive opencodex. It +can be answered without building anything, and the answer is no. + +## What was measured + +A real `muse login` was run twice on this machine (Muse Code 1.0.2, installed from +`https://dev.meta.ai/install.sh`). Both reached: + +``` +Open this page to sign in: + https://auth.meta.com/oauth/device/?code= +Waiting for approval… +``` + +Neither was approved. Both processes were terminated, and a targeted check for a Muse +credential found none on this machine. + +That observation is **device-code-shaped**, and the round-2 audit was right to stop me +calling it RFC 8628. A URL carrying a user code proves a browser-approval login exists. +It does not establish the token endpoint, scopes, rotation, or expiry semantics, and it +certainly does not establish that another client may hold the result. `002` records the +observation with that narrower framing. + +## Why this closes rather than waits + +The first revision of this doc planned to measure whether the stored credential +authenticates `https://api.meta.ai/v1`, and to ship an OAuth provider if it did. The +round-2 reviewer named the flaw in one line, and it is correct: + +> Endpoint acceptance does not override the quoted restriction that the credential is +> "for use with Muse Code only." A warning records informed risk; it does not create +> vendor authorization. + +That test was designed to discover whether enforcement was **absent** — not whether use +was **permitted**. Those are different questions, and only the second one licenses +shipping. Meta has answered the second one already (`001` §E): + +> This credential is for use with Muse Code only. Any additional API keys you create +> under your Meta Model API account will be billed through pay-as-you-go. + +A user warning does not convert a prohibited use into an allowed one; it only documents +that we knew. The goal's own wording is "**legitimately** drive a local proxy", and an +unenforced restriction is still a restriction. + +So the credential is not extracted, not replayed, and not tested against the API. That +is a deliberate stop, not an incomplete measurement. + +## The third-party report + +A Threads user (2026-09-03) reported that pay-as-you-go bills by default under the plan, +that the Muse-scoped key sits in the macOS Keychain in plaintext, and that "the endpoint +is not separated" — i.e. the CLI credential works against the general API. + +Two of those are unverified, and the third does not change the outcome even if true. +"The endpoints are not separated" is exactly the enforcement-absence observation above. +If anything it makes the recorded negative more valuable: the only thing standing +between a user and an accidental ToS breach is knowing the boundary exists. + +The billing half **is** actionable, and it is why wp1's provider note states plainly +that a Muse Code subscription does not apply and every call is metered per token. + +## Reopen conditions + +Reopen only on a first-party change. **Not** on a discovery that enforcement is loose — +that distinction is the entire finding: + +1. Meta documents the device flow for third-party clients. +2. `/docs/muse-code/subscriptions` drops the "for use with Muse Code only" scoping. +3. Meta ships a documented plan-backed API tier, as Anthropic and Kimi did. +4. Meta explicitly authorizes third-party clients on a subscription credential. + +Recheck cost is one docs read. + +## If it is ever reopened + +The plan would need what this doc deliberately does not contain: exact token endpoint +and client id, request/response types, identity/expiry/refresh semantics, an error +taxonomy, cancellation behavior, the chosen `src/oauth/.ts` filename and registry +id, and — the seam the round-2 audit caught — a `gui/src/oauth-tos-risk.ts` entry with +its `tests/oauth-tos-warning.test.ts` coverage, since that is the login-time warning +gate a provider note bypasses. Writing those against an unproven protocol would be +fabrication, so they are not written. + +## What did ship from this phase + +The disclosure in wp1's provider note, which is the user-visible half of this finding +and the part that prevents a surprise bill. diff --git a/devlog/_plan/260903_muse_spark_plan_oauth/030_wp3_delivery.md b/devlog/_plan/260903_muse_spark_plan_oauth/030_wp3_delivery.md new file mode 100644 index 0000000000..92127b9f60 --- /dev/null +++ b/devlog/_plan/260903_muse_spark_plan_oauth/030_wp3_delivery.md @@ -0,0 +1,50 @@ +# Delivery procedure + +Not a work-phase — delivery is not independently implementable, and modelling it as one +was audit blocker 8. This is the checklist each phase runs at its own C/D. + +## Shape: one PR, no stack + +``` +codex/meta-model-api-provider -> PR 1 (base: dev) wp1 +``` + +The first draft stacked two layers, then briefly claimed two independent PRs. Both were +wrong. wp2 closed as a `NOOP` negative (`020`) and ships no code, so there is one PR — +and with it, no cascade, no merge order, and no shared-constant coupling to reason +about. Branch from the current `origin/dev` tip; `dev` moved during the audit rounds. + +## Per-layer gate + +1. `git push --no-verify` (standing user instruction). +2. PR body fills every `.github/PULL_REQUEST_TEMPLATE.md` section. No GUI change, so + no screenshot is required. +3. Wait for the workflow runs on the **exact head SHA** — not the branch, the SHA. + `Cross-platform CI` plus `React Doctor`, and CodeRabbit's status. +4. Read CodeRabbit's findings. Fix anything materially wrong; record and rebut + anything that is not. A cosmetic nit does not block the merge. +5. Admin-merge (squash), pre-authorized by the user. +6. **No cascade.** One branch on `dev`. If `dev` moves under the open PR, rebase and + `git push --force-with-lease` — never a bare `--force`. + +## Verification budget + +`bun test tests/meta-model-api-provider.test.ts tests/provider-registry-parity.test.ts tests/usage-cost.test.ts`, +then `bun run test:changed`, then `bun x tsc --noEmit` and `bun run privacy:scan` (the +change ships credential guidance). + +`test:changed` is required rather than optional: `src/AGENTS.md:26` calls for it once a +touch set is broader than one file, and this one spans the registry, the price overlays, +and two test files. It follows Bun's import graph, so it is **not** the forbidden +repository-wide run. + +CI remains the full gate. If a focused run cannot cover an indirect dependency (a +subprocess, a golden file), name it in the PR's Verification section and let CI carry it +rather than reaching for the full suite. + +## Terminal outcomes + +- wp1 `DONE` when PR 1 is green at its head SHA and merged, and the registry serves + `meta-model/muse-spark-1.3` and `meta-model/muse-spark-1.3-contributor` without + capturing the existing `meta/…` Command Code selectors. +- wp2 `NOOP` — closed by a licence finding, with no code to deliver (`020`). From 793f21e87405ba80d4be704e6ce002be255b0a93 Mon Sep 17 00:00:00 2001 From: jun Date: Thu, 3 Sep 2026 11:50:00 +0900 Subject: [PATCH 2/4] feat(providers): add the direct Meta Model API provider Muse Spark reached opencodex through resellers only (command-code, opencode-go). This adds Meta's own endpoint as meta-model, an openai-responses key provider on https://api.meta.ai/v1. Built entirely from published spec. No API key was issued: the only contact with the endpoint was an unauthenticated GET /v1/models returning 401 invalid_api_key, which proves the host routes /v1 and answers in OpenAI error shape. Every value traces to a vendor page recorded in the plan's claim ledger, and anything Meta does not publish is absent rather than guessed -- notably max output tokens, whose only apparent value (131072) lives inside a third-party config sample while the protocol docs call the real limit model-dependent. Four decisions are not obvious from the vendor docs, and three of them come from this repository rather than from Meta. The id is meta-model, not meta. router.ts resolves a / prefix against configured providers first, so a provider named meta would have captured meta/muse-spark-1.3 -- a live Command Code selector merged last week -- and silently redirected a working model reference to a different vendor and a different bill. It would also have derived META_API_KEY, which is the Muse Code CLI's variable, not this API's MODEL_API_KEY. The effort ladder carries an identity wire map. Meta publishes minimal/low/medium/high/xhigh and excludes none for Muse Spark (HTTP 400); max and ultra are absent from its list entirely. But reasoning-effort.ts rewrites minimal to low unless a model-scoped map says otherwise, so the picker would have advertised an effort the wire never sent. The registry array looked correct throughout, which is why the test asserts the built Responses body instead. liveModels is false. No authenticated /v1/models payload was ever observed, and Meta serves image and voice families on this same base URL, so discovery would publish rows this Responses-agent provider cannot drive. preserveCustomDestination is set, so a user who already owns a custom provider under this id keeps their destination and their key. The note states plainly that a Muse Code subscription does NOT apply here. Meta scopes that credential to its own CLI and meters every other key per token, so a user assuming their plan covers this would be billed pay-as-you-go. The Contributor tier's ~92% discount is disclosed as what it is: Meta trains on those prompts. Also: two price overlays (meta-model has no jawcode alias, so without them the cost column renders nothing), the order-sensitive parity roster, and the docs-site provider tables. Plan, claim ledger, and the four-round audit trail: devlog/_plan/260903_muse_spark_plan_oauth/. --- .../src/content/docs/guides/providers.md | 16 ++ src/providers/registry.ts | 64 ++++++++ src/usage/expected-prices.ts | 9 + tests/meta-model-api-provider.test.ts | 154 ++++++++++++++++++ tests/provider-registry-parity.test.ts | 2 +- tests/usage-cost.test.ts | 8 +- 6 files changed, 250 insertions(+), 3 deletions(-) create mode 100644 tests/meta-model-api-provider.test.ts diff --git a/docs-site/src/content/docs/guides/providers.md b/docs-site/src/content/docs/guides/providers.md index 3a3276c602..81d38397b3 100644 --- a/docs-site/src/content/docs/guides/providers.md +++ b/docs-site/src/content/docs/guides/providers.md @@ -343,6 +343,7 @@ free-experimentation model. | Vultr Serverless Inference | `https://api.vultrinference.com/v1` | | Baseten Model APIs | `https://inference.baseten.co/v1` | | Command Code | `https://api.commandcode.ai/provider/v1` | +| Meta Model API | `https://api.meta.ai/v1` | | SambaNova Cloud | `https://api.sambanova.ai/v1` | | Nebius Token Factory | `https://api.tokenfactory.nebius.com/v1` | | DigitalOcean Serverless Inference | `https://inference.do-ai.run/v1` | @@ -437,6 +438,21 @@ preset (`commandcode`) uses the active configured Bearer key for chat requests; (`command-code`) uses the stored account bearer for authenticated discovery and chat. Create Provider-API keys at [Command Code Studio](https://commandcode.ai/studio/). +**Meta Model API (`meta-model`).** Muse Spark on Meta's own OpenAI-compatible endpoint, +served over `/v1/responses`. Set a `MODEL_API_KEY` from +[the Meta developer console](https://dev.meta.ai/docs/authentication); the account needs a +payment method before it will serve requests, and every call is metered per token. Two +models are seeded — `meta-model/muse-spark-1.3` and `meta-model/muse-spark-1.3-contributor` +— with the vendor's `minimal`/`low`/`medium`/`high`/`xhigh` ladder and a 1M context window. +Discovery stays off until an authenticated roster is verified, because Meta serves image and +voice models on the same host. + +Two things worth knowing before you pick it. **A Muse Code subscription does not apply +here:** Meta scopes that credential to the Muse Code CLI and bills any other key +pay-as-you-go. And the Contributor tier is roughly 92% cheaper because Meta trains on your +prompts, so keep confidential material off it. The same models are also reachable through +the `command-code` and `opencode-go` presets. + **Command Code quota.** The dashboard and `ocx account refresh` probe Command Code's `/alpha/billing/credits` windows (5-hour and weekly) on the canonical `https://api.commandcode.ai` host. The OAuth preset (`command-code`) uses the stored diff --git a/src/providers/registry.ts b/src/providers/registry.ts index 01b4a0fa1c..8b98c24c59 100644 --- a/src/providers/registry.ts +++ b/src/providers/registry.ts @@ -435,6 +435,32 @@ const OPENAI_API_GPT56_VIRTUAL_MODELS: Record = Object.fromEntries( + META_MUSE_REASONING_EFFORTS.map(effort => [effort, effort]), +); +/** Both Muse Spark 1.3 tiers publish a 1,048,576-token window (dev.meta.ai/docs/models). */ +const META_MUSE_CONTEXT_WINDOW = 1_048_576; +const META_MUSE_MODELS = ["muse-spark-1.3", "muse-spark-1.3-contributor"]; /** * Daybreak program aliases. These `-latest` ids are the stable contract: OpenAI repoints * them at newer snapshots over time (red -> gpt-5.6-cyber, blue -> gpt-5.6-sol as of @@ -1448,6 +1474,44 @@ export const PROVIDER_REGISTRY: readonly ProviderRegistryEntry[] = [ }, virtualModels: OPENAI_API_GPT56_VIRTUAL_MODELS, }, + /* [Decision Log] + - 목적과 의도: Reach Meta's Muse Spark models directly on Meta's own Model API, instead of only through the Command Code and OpenCode Zen resellers already in this registry. + - 기존 구현 및 제약 조건: Meta publishes both POST /v1/responses and POST /v1/chat/completions at https://api.meta.ai/v1, and no API key was issued for this change — every value here comes from the published spec (devlog/_plan/260903_muse_spark_plan_oauth/001). + - 검토한 주요 대안: register as openai-chat; use provider id "meta"; enable live discovery; wire the Muse Code subscription credential as OAuth. + - 선택한 방식: an openai-responses key provider under the id "meta-model", with a static two-model roster and no OAuth. + - 다른 대안 대신 이 방식을 선택한 이유: Meta calls Responses "the recommended default for new work ... OpenAI-compatible and exposes the full feature set", carrying reasoning replay and native input_image that Chat would forfeit. The id is "meta-model" because "meta" would capture the LIVE Command Code selector meta/muse-spark-1.3 at router.ts's provider-prefix branch, and would derive META_API_KEY — the Muse Code CLI's variable, not this API's MODEL_API_KEY. + - 장점, 단점 및 영향: users reach Muse Spark without a reseller; discovery stays off until an authenticated /v1/models payload is actually observed, so an unseen roster (Meta also serves image and voice families here) cannot leak into the picker. + */ + { + id: "meta-model", + label: "Meta Model API", + adapter: "openai-responses", + baseUrl: "https://api.meta.ai/v1", + authKind: "key", + dashboardUrl: "https://dev.meta.ai/docs/authentication", + defaultModel: "muse-spark-1.3", + models: META_MUSE_MODELS, + // Static roster: no authenticated /v1/models payload was ever observed (the only + // contact was an unauthenticated GET returning 401 invalid_api_key), and Meta serves + // non-agent families on this same base URL. Turning discovery on would publish an + // unseen roster into the picker. + liveModels: false, + // A user may already own a custom provider named "meta-model" pointing elsewhere; + // without this, registry transport canonicalization would retarget it and send their + // saved key to Meta. + preserveCustomDestination: true, + modelContextWindows: Object.fromEntries(META_MUSE_MODELS.map(id => [id, META_MUSE_CONTEXT_WINDOW])), + // text+image only. Meta also documents video, audio (degraded on 1.3), and PDF, but + // the catalog modality enum is text/image and over-advertising poisons the exported + // client config (see tests/catalog-input-modality-enum.test.ts). + modelInputModalities: Object.fromEntries(META_MUSE_MODELS.map(id => [id, ["text", "image"] as ["text", "image"]])), + modelReasoningEfforts: Object.fromEntries(META_MUSE_MODELS.map(id => [id, META_MUSE_REASONING_EFFORTS])), + modelReasoningEffortMap: Object.fromEntries(META_MUSE_MODELS.map(id => [id, META_MUSE_REASONING_EFFORT_MAP])), + // No defaultMaxOutputTokens: Meta publishes none. The only number in its docs + // (131072) appears inside a third-party config sample, and the protocol pages call + // the real limit "model-dependent". + note: "Pay-as-you-go Meta Model API. Get a key at https://dev.meta.ai — a Meta developer account needs a payment method before it can serve requests, and every call is metered per token. A Muse Code subscription does NOT work here: Meta scopes that credential to the Muse Code CLI and bills any other key pay-as-you-go (dev.meta.ai/docs/muse-code/subscriptions). The Contributor tier (muse-spark-1.3-contributor) is ~92% cheaper because Meta trains on your prompts; do not send confidential material through it. Muse Spark is also reachable through the command-code and opencode-go providers.", + }, { id: "umans", label: "Umans AI Coding Plan", diff --git a/src/usage/expected-prices.ts b/src/usage/expected-prices.ts index fee0af8dd6..20ba598569 100644 --- a/src/usage/expected-prices.ts +++ b/src/usage/expected-prices.ts @@ -88,6 +88,7 @@ const GEMINI_37_PRICING = "https://ai.google.dev/gemini-api/docs/pricing (2026-0 const GEMINI_38_PRICING = "https://ai.google.dev/gemini-api/docs/pricing (2026-09-03); promotional rate through 2026-12-31, rises to 1.50/7.50 on 2027-01-01; cacheWrite=0: storage is billed per-hour, not per-token"; const MINIMAX_PRICING = "https://platform.minimax.io/docs/guides/pricing-paygo"; const OPENAI_GPT56_PRICING = "https://developers.openai.com/api/docs/pricing"; +const META_MODEL_PRICING = "https://dev.meta.ai/docs/pricing-rate-limits"; const DEEPSEEK_PRICING = "https://api-docs.deepseek.com/quick_start/pricing-details-usd; V4 Flash alias transition scheduled 2026-07-24 — re-verify after"; // Kimi official tables publish input/output/cache-hit only; cacheWrite is mapped to the // cache-miss input price (Kimi auto-caches with no separate write billing). 2026-07-20 re-verified. @@ -145,6 +146,14 @@ export const EXPECTED_PRICE_OVERLAYS: readonly ExpectedPriceOverlay[] = [ { provider: "openai-apikey", modelId: "gpt-5.6-sol-pro", cost4: GPT56_SOL, source: `collapsed base ID ${OPENAI_GPT56_PRICING}`, verifiedAt: "2026-08-03", status: "verified-derived" }, { provider: "openai-apikey", modelId: "gpt-5.6-terra-pro", cost4: GPT56_TERRA, source: `collapsed base ID ${OPENAI_GPT56_PRICING}`, verifiedAt: "2026-08-03", status: "verified-derived" }, { provider: "openai-apikey", modelId: "gpt-5.6-luna-pro", cost4: GPT56_LUNA, source: `collapsed base ID ${OPENAI_GPT56_PRICING}`, verifiedAt: "2026-08-03", status: "verified-derived" }, + // Meta Model API direct provider. `meta-model` has no jawcode metadata alias, so an + // unpriced row falls through the whole resolution chain and the Logs cost column + // renders nothing — these exact overlays are the only source. Both are Meta's own + // published list prices for Meta's own endpoint (hence "verified", not derived), and + // they match the figures Command Code republishes for the same two models. + // cacheWrite=0: Meta publishes a cached-input price but no cache-write charge. + { provider: "meta-model", modelId: "muse-spark-1.3", cost4: { input: 1.25, output: 4.25, cacheRead: 0.15, cacheWrite: 0 }, source: `Meta Model API published price ${META_MODEL_PRICING}`, verifiedAt: "2026-09-03", status: "verified" }, + { provider: "meta-model", modelId: "muse-spark-1.3-contributor", cost4: { input: 0.1, output: 0.2, cacheRead: 0.002, cacheWrite: 0 }, source: `Meta Model API published Contributor-tier price ${META_MODEL_PRICING}; data-sharing discount tier`, verifiedAt: "2026-09-03", status: "verified" }, // Daybreak aliases: priced as their current snapshots (red -> gpt-5.6-cyber, // blue -> gpt-5.6-sol). The alias ids carry no rows of their own upstream, hence // verified-derived. Blue deliberately reuses GPT56_SOL rather than duplicating the tuple. diff --git a/tests/meta-model-api-provider.test.ts b/tests/meta-model-api-provider.test.ts new file mode 100644 index 0000000000..deb462874b --- /dev/null +++ b/tests/meta-model-api-provider.test.ts @@ -0,0 +1,154 @@ +/** + * Meta Model API direct provider (`meta-model`). + * + * Muse Spark reached opencodex through resellers first (Command Code, OpenCode Zen). + * This entry adds Meta's own endpoint, built entirely from published spec — no API key + * was issued, so every value here is a documented claim rather than a probe result. + * + * Three of these tests exist because a registry-shape assertion alone would have passed + * while the runtime was wrong: + * + * - the provider id would have captured a LIVE reseller model namespace at route time; + * - the advertised `minimal` effort would have been rewritten to `low` on the wire; + * - the note carrying the billing disclosure had no regression at all. + */ +import { describe, expect, test } from "bun:test"; +import { createResponsesPassthroughAdapter as createResponsesPassthroughAdapterProduction } from "../src/adapters/openai-responses"; +import { enrichProviderFromRegistry, providerConfigSeed } from "../src/providers/derive"; +import { getProviderRegistryEntry } from "../src/providers/registry"; +import { routeModel } from "../src/router"; +import type { OcxConfig, OcxProviderConfig } from "../src/types"; +import { withTestTranslatorBudget } from "./helpers/translator-budget"; + +const createResponsesPassthroughAdapter = (...args: Parameters) => + withTestTranslatorBudget(createResponsesPassthroughAdapterProduction(...args)); + +const META_MODELS = ["muse-spark-1.3", "muse-spark-1.3-contributor"] as const; + +function entry() { + const found = getProviderRegistryEntry("meta-model"); + if (!found) throw new Error("missing meta-model registry entry"); + return found; +} + +describe("Meta Model API provider (meta-model)", () => { + test("routes to the published OpenAI-compatible Responses base URL", () => { + expect(entry().baseUrl).toBe("https://api.meta.ai/v1"); + expect(entry().adapter).toBe("openai-responses"); + expect(entry().authKind).toBe("key"); + expect(entry().defaultModel).toBe("muse-spark-1.3"); + }); + + test("advertises exactly the vendor's effort ladder", () => { + for (const id of META_MODELS) { + expect(entry().modelReasoningEfforts?.[id]).toEqual(["minimal", "low", "medium", "high", "xhigh"]); + } + }); + + test("never advertises an effort the vendor rejects", () => { + // `none` returns HTTP 400 on Muse Spark; `max`/`ultra` are absent from the + // published set entirely, and an unauthenticated Zen probe rejected all three. + const efforts = entry().modelReasoningEfforts?.["muse-spark-1.3"] ?? []; + for (const forbidden of ["none", "max", "ultra"]) expect(efforts).not.toContain(forbidden); + }); + + test("declares the published 1M window and text+image only", () => { + for (const id of META_MODELS) { + expect(entry().modelContextWindows?.[id]).toBe(1_048_576); + // Meta also documents video, audio and PDF; the catalog modality enum is + // text/image and over-advertising poisons the exported client config. + expect(entry().modelInputModalities?.[id]).toEqual(["text", "image"]); + } + }); + + test("claims no max-output limit, because the vendor publishes none", () => { + expect(entry().defaultMaxOutputTokens).toBeUndefined(); + }); + + test("keeps live discovery off until an authenticated roster is observed", () => { + // The only contact with /v1/models was an unauthenticated 401. Meta serves image + // and voice families on this same base URL, so discovery would publish rows this + // Responses-agent provider cannot drive. + expect(entry().liveModels).toBeFalsy(); + expect(entry().models).toEqual([...META_MODELS]); + }); + + test("the seed survives derive() intact", () => { + const seed = providerConfigSeed(entry()); + expect(seed.baseUrl).toBe("https://api.meta.ai/v1"); + expect(seed.modelContextWindows?.["muse-spark-1.3"]).toBe(1_048_576); + expect(seed.modelReasoningEfforts?.["muse-spark-1.3"]).toEqual(["minimal", "low", "medium", "high", "xhigh"]); + }); + + /* + * The namespace-theft regression. `meta/muse-spark-1.3` is a live Command Code + * selector; the router resolves a `/` prefix against configured + * providers, so an id of `meta` would have silently redirected an already-working + * model reference to a different vendor and a different bill. + */ + test("meta/muse-spark-1.3 still reaches command-code with the direct provider configured", () => { + const config: OcxConfig = { + port: 10100, + defaultProvider: "command-code", + providers: { + "command-code": { adapter: "command-code", baseUrl: "https://api.commandcode.ai", apiKey: "cc-test-key", authMode: "key" }, + "meta-model": { adapter: "openai-responses", baseUrl: "https://api.meta.ai/v1", apiKey: "meta-test-key", authMode: "key" }, + }, + }; + const route = routeModel(config, "meta/muse-spark-1.3"); + expect(route.providerName).toBe("command-code"); + expect(route.modelId).toBe("meta/muse-spark-1.3"); + + // The direct provider is still reachable under its own prefix. + expect(routeModel(config, "meta-model/muse-spark-1.3").providerName).toBe("meta-model"); + }); + + /* + * The wire-serialization regression. src/reasoning-effort.ts rewrites `minimal` to + * `low` unless a model-scoped wire map says otherwise — so this asserts the built + * request body, not the registry array that looked correct throughout. + */ + test("minimal reaches the wire as minimal, not low", () => { + const provider = { ...providerConfigSeed(entry()), apiKey: "meta-test-key" } as OcxProviderConfig; + const request = createResponsesPassthroughAdapter(provider).buildRequest({ + modelId: "muse-spark-1.3", + context: { messages: [] }, + stream: false, + options: { reasoning: "minimal" }, + _rawBody: { model: "muse-spark-1.3", input: "ping", reasoning: { effort: "minimal" } }, + }, { headers: new Headers() }); + const body = JSON.parse(request.body) as { reasoning?: { effort?: string } }; + expect(body.reasoning?.effort).toBe("minimal"); + }); + + /* + * A user may already own a custom provider under this id pointing elsewhere. + * preserveCustomDestination stops registry canonicalization from retargeting it and + * sending their saved key to Meta. + */ + test("a same-named custom provider keeps its own destination", () => { + expect(entry().preserveCustomDestination).toBe(true); + const custom: OcxProviderConfig = { + adapter: "openai-chat", + baseUrl: "https://internal.example/v1", + apiKey: "someone-elses-key", + authMode: "key", + } as OcxProviderConfig; + enrichProviderFromRegistry("meta-model", custom); + expect(custom.baseUrl).toBe("https://internal.example/v1"); + expect(custom.apiKey).toBe("someone-elses-key"); + }); + + /* + * The note is load-bearing, not decoration. A user holding a Muse Code subscription + * will otherwise assume it applies here and get billed pay-as-you-go instead; Meta + * scopes that credential to its own CLI. Deleting this text removes the only + * in-product warning. + */ + test("the note discloses the subscription boundary and the Contributor training tradeoff", () => { + const note = entry().note ?? ""; + expect(note).toContain("Muse Code subscription does NOT work here"); + expect(note).toContain("metered per token"); + expect(note.toLowerCase()).toContain("trains on your prompts"); + }); +}); diff --git a/tests/provider-registry-parity.test.ts b/tests/provider-registry-parity.test.ts index 573af1b27b..b52a57583a 100644 --- a/tests/provider-registry-parity.test.ts +++ b/tests/provider-registry-parity.test.ts @@ -31,7 +31,7 @@ function nativeTemplate(): Record { } const EXPECTED_KEY_PROVIDER_IDS = [ - "anthropic-apikey", "openai-apikey", "umans", "opencode-go", "neuralwatt", "openrouter", "cline-pass", "cline", "orcarouter", "bizrouter", "groq", "google", "google-vertex", "azure-openai", + "anthropic-apikey", "openai-apikey", "meta-model", "umans", "opencode-go", "neuralwatt", "openrouter", "cline-pass", "cline", "orcarouter", "bizrouter", "groq", "google", "google-vertex", "azure-openai", "deepseek", "cerebras", "chutes", "deepinfra", "hyperbolic", "nscale", "vultr", "baseten", "commandcode", "sambanova", "nebius", "digitalocean", "scaleway", "featherless", "novita", "together", "fireworks", "firepass", "moonshot", "huggingface", "nvidia", "venice", "zai", "zhipu-bigmodel", "zhipu-bigmodel-coding", "nanogpt", "synthetic", "siliconflow", "qwen-cloud", "tencent-coding-plan", "volcengine", "volcengine-coding-plan", "volcengine-agent-plan", "qianfan", "alibaba", "alibaba-token-plan", "alibaba-token-plan-intl", "parallel", "zenmux", "litellm", "ollama-cloud", "mistral", diff --git a/tests/usage-cost.test.ts b/tests/usage-cost.test.ts index 14d839894a..d93ae813c4 100644 --- a/tests/usage-cost.test.ts +++ b/tests/usage-cost.test.ts @@ -297,8 +297,8 @@ describe("resolveMatchedPrice", () => { expect(resolveMatchedPrice("openrouter", "anthropic-claude-3.5-sonnet")).toBeNull(); }); - test("16. shipped overlay membership: 64 keys, including canonical Fable 5.1, Opus 5 and compatibility prices", () => { - expect(EXPECTED_PRICE_OVERLAYS.length).toBe(64); + test("16. shipped overlay membership: 66 keys, including canonical Fable 5.1, Opus 5 and compatibility prices", () => { + expect(EXPECTED_PRICE_OVERLAYS.length).toBe(66); expect(EXPECTED_PRICE_OVERLAYS.some(row => row.status === "unverified")).toBe(false); const keys = new Set(EXPECTED_PRICE_OVERLAYS.map(row => `${row.provider}/${row.modelId}`)); for (const expected of [ @@ -318,6 +318,10 @@ describe("resolveMatchedPrice", () => { "google-antigravity/gemini-3.8-flash", "google-antigravity/gemini-3.8-flash-low", "google-antigravity/gemini-3.8-flash-medium", + // meta-model has no jawcode alias, so these exact overlays are the only price + // source for the direct Meta provider. + "meta-model/muse-spark-1.3", + "meta-model/muse-spark-1.3-contributor", "google-antigravity/gemini-3.8-flash-high", "google/gemini-3.8-flash", "google-antigravity/gemini-3.1-pro-low", From 37f056fd065876d52d754d0ba876886dce953903 Mon Sep 17 00:00:00 2001 From: jun Date: Thu, 3 Sep 2026 12:06:18 +0900 Subject: [PATCH 3/4] fix(providers): name the env var opencodex actually reads for Meta CodeRabbit review findings on #3321. Two were user-facing defects. The docs told a user to set MODEL_API_KEY, which is Meta's own name for the variable. opencodex derives the env var from the provider id, so it reads META_MODEL_API_KEY and would never have seen the key. Following the docs verbatim produced a provider that silently failed to authenticate. Both the provider note and the docs-site section now give Meta's name and the one this proxy reads. The Contributor tier was described as '~92% cheaper', which is true only of input. The published prices are about 92% off input, 95% off output, and 99% off cached input, and a single tier-wide figure understates the saving while sounding like a precise claim. All three are stated now. Three documentation findings, all accepted: - 002 asserted that the credential muse login stores IS the subscription credential. No login completed, so that was never measured. It is now stated as inferred from Meta's description of CLI onboarding, with the reason the conclusion holds either way: muse login is the Muse Code CLI's own sign-in, so whatever it yields has no stronger claim to third-party use. - The verification gate differed across three plan documents. 030 now holds one canonical gate and the others reference it, including the docs-site frozen-lockfile install and build that docs-site/AGENTS.md requires whenever that directory is touched. Verified: 417 pages built. - A test-count sentence said six where the block has seven. --- .../260903_muse_spark_plan_oauth/000_plan.md | 2 +- .../002_plan_credential_feasibility.md | 12 +++++++++--- .../010_wp1_direct_provider.md | 10 +++++++++- .../030_wp3_delivery.md | 16 +++++++++++++--- docs-site/src/content/docs/guides/providers.md | 13 ++++++++----- src/providers/registry.ts | 5 ++++- 6 files changed, 44 insertions(+), 14 deletions(-) diff --git a/devlog/_plan/260903_muse_spark_plan_oauth/000_plan.md b/devlog/_plan/260903_muse_spark_plan_oauth/000_plan.md index e97780484e..8205510b6a 100644 --- a/devlog/_plan/260903_muse_spark_plan_oauth/000_plan.md +++ b/devlog/_plan/260903_muse_spark_plan_oauth/000_plan.md @@ -11,7 +11,7 @@ - Trigger: the user asked whether Meta's Muse Spark *plan* can be attached, after `878f75417` landed Muse Spark 1.3 through the Command Code and OpenCode Zen resellers. - Goal: reach Muse Spark **directly** on Meta's own endpoint, and settle — with evidence, not inference — whether a Muse Code subscription can legitimately drive a local proxy. - Non-goals: issuing a Meta API key, entering payment details, touching generated metadata, changing any `*-free` Zen id, retiring or redefaulting any model, altering the merged 1.3 work. -- Verifier: focused `bun test ` on touched suites plus `bun x tsc --noEmit`. **The repository-wide local suite is forbidden by standing user instruction**; exact-head GitHub CI is the authoritative gate. +- Verifier: the canonical gate in `030` — focused `bun test` on the touched suites, `bun run test:changed`, `bun x tsc --noEmit`, `bun run privacy:scan`, and the `docs-site` frozen-lockfile install plus build. **The repository-wide local suite is forbidden by standing user instruction**; exact-head GitHub CI is the authoritative gate. - Stop condition: every work-phase closed and the single implementation PR green on its exact head SHA and merged into `dev`. - Memory artifact: this unit folder. - Terminal outcomes: wp1 targets `DONE`; wp2 closed `NOOP` on a licence finding. `BLOCKED` remains available if CI or branch protection refuses for an unrelated reason. diff --git a/devlog/_plan/260903_muse_spark_plan_oauth/002_plan_credential_feasibility.md b/devlog/_plan/260903_muse_spark_plan_oauth/002_plan_credential_feasibility.md index 586ea84a38..44211b44df 100644 --- a/devlog/_plan/260903_muse_spark_plan_oauth/002_plan_credential_feasibility.md +++ b/devlog/_plan/260903_muse_spark_plan_oauth/002_plan_credential_feasibility.md @@ -105,9 +105,15 @@ observation above. ## The licence question is the whole answer `/docs/muse-code/subscriptions` says the subscription credential is "for use with Muse -Code only", and a credential obtained by `muse login` is that credential. Mechanism and -entitlement are separable questions, and only entitlement decides whether anything -ships. The vendor has answered it. +Code only". Whether the artifact `muse login` stores **is** that credential was never +measured — no login completed — so the link is inferred from Meta's own description of +the CLI onboarding, not proven here. It does not need to be proven: `muse login` is the +Muse Code CLI's own sign-in, so any credential it yields is at best that credential and +at worst something with even less claim to third-party use. Either way the restriction +binds. + +Mechanism and entitlement are separable questions, and only entitlement decides whether +anything ships. The vendor has answered it. `src/oauth/index.ts` already carries the adjacent precedent on Anthropic — `defaultRefreshPolicy: "disabled"`, with a comment recording that the vendor diff --git a/devlog/_plan/260903_muse_spark_plan_oauth/010_wp1_direct_provider.md b/devlog/_plan/260903_muse_spark_plan_oauth/010_wp1_direct_provider.md index 24285c3f08..89e49e1e05 100644 --- a/devlog/_plan/260903_muse_spark_plan_oauth/010_wp1_direct_provider.md +++ b/devlog/_plan/260903_muse_spark_plan_oauth/010_wp1_direct_provider.md @@ -169,7 +169,7 @@ commit and add exact-lookup assertions for both ids. ## NEW `tests/meta-model-api-provider.test.ts` -Six assertions, each pinning a ledger row that a future edit could silently break: +Seven assertions, each pinning a ledger row that a future edit could silently break: ```ts import { describe, expect, test } from "bun:test"; @@ -260,5 +260,13 @@ locales are left alone rather than machine-guessed. than one file; it is import-graph-scoped, not the forbidden repository-wide suite), then `bun x tsc --noEmit` and `bun run privacy:scan` (this change ships credential guidance). +Because the touch set includes `docs-site/`, `docs-site/AGENTS.md` additionally requires +the site build — "do not claim documentation validation passed unless this build +completes successfully": + +```bash +cd docs-site && bun install --frozen-lockfile && bun run build +``` + Branch from the current `origin/dev` tip, not from a remembered SHA: `dev` moved during the audit rounds. diff --git a/devlog/_plan/260903_muse_spark_plan_oauth/030_wp3_delivery.md b/devlog/_plan/260903_muse_spark_plan_oauth/030_wp3_delivery.md index 92127b9f60..3730569dd6 100644 --- a/devlog/_plan/260903_muse_spark_plan_oauth/030_wp3_delivery.md +++ b/devlog/_plan/260903_muse_spark_plan_oauth/030_wp3_delivery.md @@ -29,9 +29,19 @@ about. Branch from the current `origin/dev` tip; `dev` moved during the audit ro ## Verification budget -`bun test tests/meta-model-api-provider.test.ts tests/provider-registry-parity.test.ts tests/usage-cost.test.ts`, -then `bun run test:changed`, then `bun x tsc --noEmit` and `bun run privacy:scan` (the -change ships credential guidance). +The canonical gate for this unit, in order: + +```bash +bun test tests/meta-model-api-provider.test.ts tests/provider-registry-parity.test.ts tests/usage-cost.test.ts +bun run test:changed +bun x tsc --noEmit +bun run privacy:scan +cd docs-site && bun install --frozen-lockfile && bun run build +``` + +The last line applies because the touch set includes `docs-site/`; +`docs-site/AGENTS.md` treats that build as the documentation gate. `privacy:scan` runs +because the change ships credential guidance. `test:changed` is required rather than optional: `src/AGENTS.md:26` calls for it once a touch set is broader than one file, and this one spans the registry, the price overlays, diff --git a/docs-site/src/content/docs/guides/providers.md b/docs-site/src/content/docs/guides/providers.md index 81d38397b3..d7a8609777 100644 --- a/docs-site/src/content/docs/guides/providers.md +++ b/docs-site/src/content/docs/guides/providers.md @@ -439,8 +439,10 @@ preset (`commandcode`) uses the active configured Bearer key for chat requests; Provider-API keys at [Command Code Studio](https://commandcode.ai/studio/). **Meta Model API (`meta-model`).** Muse Spark on Meta's own OpenAI-compatible endpoint, -served over `/v1/responses`. Set a `MODEL_API_KEY` from -[the Meta developer console](https://dev.meta.ai/docs/authentication); the account needs a +served over `/v1/responses`. Create a key in +[the Meta developer console](https://dev.meta.ai/docs/authentication) — Meta calls this +variable `MODEL_API_KEY`, but opencodex derives the env var from the provider id, so +export it as **`META_MODEL_API_KEY`** (or paste it during `ocx init`). The account needs a payment method before it will serve requests, and every call is metered per token. Two models are seeded — `meta-model/muse-spark-1.3` and `meta-model/muse-spark-1.3-contributor` — with the vendor's `minimal`/`low`/`medium`/`high`/`xhigh` ladder and a 1M context window. @@ -449,9 +451,10 @@ voice models on the same host. Two things worth knowing before you pick it. **A Muse Code subscription does not apply here:** Meta scopes that credential to the Muse Code CLI and bills any other key -pay-as-you-go. And the Contributor tier is roughly 92% cheaper because Meta trains on your -prompts, so keep confidential material off it. The same models are also reachable through -the `command-code` and `opencode-go` presets. +pay-as-you-go. And the Contributor tier is cheap because Meta trains on your prompts — +roughly 92% off input, 95% off output, and 99% off cached input — so keep confidential +material off it. The same models are also reachable through the `command-code` and +`opencode-go` presets. **Command Code quota.** The dashboard and `ocx account refresh` probe Command Code's `/alpha/billing/credits` windows (5-hour and weekly) on the canonical diff --git a/src/providers/registry.ts b/src/providers/registry.ts index 8b98c24c59..d44790b7f1 100644 --- a/src/providers/registry.ts +++ b/src/providers/registry.ts @@ -1510,7 +1510,10 @@ export const PROVIDER_REGISTRY: readonly ProviderRegistryEntry[] = [ // No defaultMaxOutputTokens: Meta publishes none. The only number in its docs // (131072) appears inside a third-party config sample, and the protocol pages call // the real limit "model-dependent". - note: "Pay-as-you-go Meta Model API. Get a key at https://dev.meta.ai — a Meta developer account needs a payment method before it can serve requests, and every call is metered per token. A Muse Code subscription does NOT work here: Meta scopes that credential to the Muse Code CLI and bills any other key pay-as-you-go (dev.meta.ai/docs/muse-code/subscriptions). The Contributor tier (muse-spark-1.3-contributor) is ~92% cheaper because Meta trains on your prompts; do not send confidential material through it. Muse Spark is also reachable through the command-code and opencode-go providers.", + // Meta names its variable MODEL_API_KEY, but the env var opencodex reads is derived + // from the provider id (META_MODEL_API_KEY). Saying only Meta's name would send a + // user to export a variable this proxy never reads. + note: "Pay-as-you-go Meta Model API. Get a key at https://dev.meta.ai (Meta calls it MODEL_API_KEY; export it here as META_MODEL_API_KEY) — a Meta developer account needs a payment method before it can serve requests, and every call is metered per token. A Muse Code subscription does NOT work here: Meta scopes that credential to the Muse Code CLI and bills any other key pay-as-you-go (dev.meta.ai/docs/muse-code/subscriptions). The Contributor tier (muse-spark-1.3-contributor) is cheap because Meta trains on your prompts — about 92% off input, 95% off output, 99% off cached input; do not send confidential material through it. Muse Spark is also reachable through the command-code and opencode-go providers.", }, { id: "umans", From b9adea5f2e0244da8a8acc9ac6cac40581befb37 Mon Sep 17 00:00:00 2001 From: jun Date: Thu, 3 Sep 2026 12:20:59 +0900 Subject: [PATCH 4/4] fix(docs): narrow the reseller claim for Muse Spark CodeRabbit second-pass finding on #3321, and it is right: opencode-go serves only muse-spark-1.3-contributor, not the standard tier. Saying 'the same models are also reachable' would send a user to a preset that does not carry the model they were reading about. Both the provider note and the docs-site section now name which reseller carries which tier: command-code has both, opencode-go has Contributor only. Also corrects 'seven assertions' to 'seven tests' in the plan doc; the block has seven test cases and more than seven expect calls. --- .../260903_muse_spark_plan_oauth/010_wp1_direct_provider.md | 2 +- docs-site/src/content/docs/guides/providers.md | 5 +++-- src/providers/registry.ts | 2 +- 3 files changed, 5 insertions(+), 4 deletions(-) diff --git a/devlog/_plan/260903_muse_spark_plan_oauth/010_wp1_direct_provider.md b/devlog/_plan/260903_muse_spark_plan_oauth/010_wp1_direct_provider.md index 89e49e1e05..23d9dc6aad 100644 --- a/devlog/_plan/260903_muse_spark_plan_oauth/010_wp1_direct_provider.md +++ b/devlog/_plan/260903_muse_spark_plan_oauth/010_wp1_direct_provider.md @@ -169,7 +169,7 @@ commit and add exact-lookup assertions for both ids. ## NEW `tests/meta-model-api-provider.test.ts` -Seven assertions, each pinning a ledger row that a future edit could silently break: +Seven tests, each pinning a ledger row that a future edit could silently break: ```ts import { describe, expect, test } from "bun:test"; diff --git a/docs-site/src/content/docs/guides/providers.md b/docs-site/src/content/docs/guides/providers.md index d7a8609777..eaea71d0bc 100644 --- a/docs-site/src/content/docs/guides/providers.md +++ b/docs-site/src/content/docs/guides/providers.md @@ -453,8 +453,9 @@ Two things worth knowing before you pick it. **A Muse Code subscription does not here:** Meta scopes that credential to the Muse Code CLI and bills any other key pay-as-you-go. And the Contributor tier is cheap because Meta trains on your prompts — roughly 92% off input, 95% off output, and 99% off cached input — so keep confidential -material off it. The same models are also reachable through the `command-code` and -`opencode-go` presets. +material off it. Muse Spark is also reachable through resellers, with a narrower roster: +`command-code` carries both tiers, while `opencode-go` serves only +`muse-spark-1.3-contributor`. **Command Code quota.** The dashboard and `ocx account refresh` probe Command Code's `/alpha/billing/credits` windows (5-hour and weekly) on the canonical diff --git a/src/providers/registry.ts b/src/providers/registry.ts index d44790b7f1..20c1befd76 100644 --- a/src/providers/registry.ts +++ b/src/providers/registry.ts @@ -1513,7 +1513,7 @@ export const PROVIDER_REGISTRY: readonly ProviderRegistryEntry[] = [ // Meta names its variable MODEL_API_KEY, but the env var opencodex reads is derived // from the provider id (META_MODEL_API_KEY). Saying only Meta's name would send a // user to export a variable this proxy never reads. - note: "Pay-as-you-go Meta Model API. Get a key at https://dev.meta.ai (Meta calls it MODEL_API_KEY; export it here as META_MODEL_API_KEY) — a Meta developer account needs a payment method before it can serve requests, and every call is metered per token. A Muse Code subscription does NOT work here: Meta scopes that credential to the Muse Code CLI and bills any other key pay-as-you-go (dev.meta.ai/docs/muse-code/subscriptions). The Contributor tier (muse-spark-1.3-contributor) is cheap because Meta trains on your prompts — about 92% off input, 95% off output, 99% off cached input; do not send confidential material through it. Muse Spark is also reachable through the command-code and opencode-go providers.", + note: "Pay-as-you-go Meta Model API. Get a key at https://dev.meta.ai (Meta calls it MODEL_API_KEY; export it here as META_MODEL_API_KEY) — a Meta developer account needs a payment method before it can serve requests, and every call is metered per token. A Muse Code subscription does NOT work here: Meta scopes that credential to the Muse Code CLI and bills any other key pay-as-you-go (dev.meta.ai/docs/muse-code/subscriptions). The Contributor tier (muse-spark-1.3-contributor) is cheap because Meta trains on your prompts — about 92% off input, 95% off output, 99% off cached input; do not send confidential material through it. Muse Spark is also reachable through resellers: command-code carries both tiers, opencode-go serves only muse-spark-1.3-contributor.", }, { id: "umans",