From dbaeb5e2380f49050ba196430cf50a9fc6e62fb6 Mon Sep 17 00:00:00 2001 From: Daniil Koryto Date: Tue, 18 Aug 2026 15:10:12 +0300 Subject: [PATCH 1/3] feat(models): add DeepSeek V4 Flash 0731 as the default GonkaGate model Add `deepseek-ai/deepseek-v4-flash-0731` (400K context, chat_completions, @ai-sdk/openai-compatible) to the curated model registry as the single recommended entry and demote `moonshotai/kimi-k2.6` to `recommended: false` so `createCuratedModelIndex` keeps its at-most-one-recommended-validated invariant. DeepSeek stays `validationStatus: "candidate"` because no MiMoCode workflow proof exists for it yet. Docs, docs-contract, and package-contract tests are updated to match. Co-Authored-By: Claude Opus 5 --- docs/how-it-works.md | 7 +++++++ docs/model-validation.md | 6 ++++++ src/constants/models.ts | 13 ++++++++++++- test/docs-contract.test.ts | 2 ++ test/package-contract.test.ts | 28 ++++++++++++++++++++-------- 5 files changed, 47 insertions(+), 9 deletions(-) diff --git a/docs/how-it-works.md b/docs/how-it-works.md index 1a38e53..cda3f53 100644 --- a/docs/how-it-works.md +++ b/docs/how-it-works.md @@ -64,6 +64,13 @@ The intended managed provider shape is: "setCacheKey": false }, "models": { + "deepseek-ai/deepseek-v4-flash-0731": { + "name": "deepseek-ai/deepseek-v4-flash-0731", + "limit": { + "context": 0, + "output": 0 + } + }, "moonshotai/kimi-k2.6": { "name": "moonshotai/kimi-k2.6", "limit": { diff --git a/docs/model-validation.md b/docs/model-validation.md index d003a80..eb918a1 100644 --- a/docs/model-validation.md +++ b/docs/model-validation.md @@ -10,6 +10,7 @@ Current MiMoCode-validated workflow proof exists for: The current public GonkaGate models page also lists: +- `deepseek-ai/deepseek-v4-flash-0731` - DeepSeek V4 Flash 0731, 400K context. - `minimaxai/minimax-m2.7` - MiniMax M2.7, 180K context. - `qwen/qwen3-235b-a22b-instruct-2507-fp8` - Qwen3 235B A22B Instruct 2507 FP8, 240K context. @@ -17,6 +18,11 @@ The current public GonkaGate models page also lists: GonkaGate `/v1/models` availability is setup-catalog proof, not full MiMoCode workflow validation proof. +`deepseek-ai/deepseek-v4-flash-0731` is the recommended curated entry in +`src/constants/models.ts`, so it is the model the curated registry points new +setups at. It has no MiMoCode workflow proof yet, so it stays `candidate` and +the curated registry currently exposes no recommended validated model. + Live MiMoCode validation for Kimi uses the full GonkaGate slug as the MiMoCode model key, so the effective model ref is `gonkagate/moonshotai/kimi-k2.6`. Short aliases such as diff --git a/src/constants/models.ts b/src/constants/models.ts index 29d6b23..83bb77b 100644 --- a/src/constants/models.ts +++ b/src/constants/models.ts @@ -94,6 +94,17 @@ export type MimoCodeModelRef = `gonkagate/${TKey}`; export const CURATED_MODEL_REGISTRY = Object.freeze({ + "deepseek-ai/deepseek-v4-flash-0731": { + adapterPackage: "@ai-sdk/openai-compatible", + displayName: "DeepSeek V4 Flash 0731", + limits: { + context: 400_000, + }, + modelId: "deepseek-ai/deepseek-v4-flash-0731", + recommended: true, + transport: "chat_completions", + validationStatus: "candidate", + }, "moonshotai/kimi-k2.6": { adapterPackage: "@ai-sdk/openai-compatible", displayName: "Kimi K2.6", @@ -101,7 +112,7 @@ export const CURATED_MODEL_REGISTRY = Object.freeze({ context: 262_000, }, modelId: "moonshotai/kimi-k2.6", - recommended: true, + recommended: false, transport: "chat_completions", validationStatus: "validated", }, diff --git a/test/docs-contract.test.ts b/test/docs-contract.test.ts index 01d64c4..0022577 100644 --- a/test/docs-contract.test.ts +++ b/test/docs-contract.test.ts @@ -89,7 +89,9 @@ test("model validation docs separate live catalog availability from workflow pro /qwen\/qwen3-235b-a22b-instruct-2507-fp8/, /moonshotai\/kimi-k2\.6/, /minimaxai\/minimax-m2\.7/, + /deepseek-ai\/deepseek-v4-flash-0731/, /public GonkaGate models page/, + /400K context/, /240K context/, /180K context/, /setCacheKey.*false/s, diff --git a/test/package-contract.test.ts b/test/package-contract.test.ts index 189a2e7..83a9bdd 100644 --- a/test/package-contract.test.ts +++ b/test/package-contract.test.ts @@ -120,26 +120,38 @@ test("constants pin the planned GonkaGate MiMoCode provider contract", () => { test("curated model registry is present but not falsely validated", () => { assert.deepEqual(SUPPORTED_MODEL_KEYS, [ + "deepseek-ai/deepseek-v4-flash-0731", "moonshotai/kimi-k2.6", "minimaxai/minimax-m2.7", "qwen/qwen3-235b-a22b-instruct-2507-fp8", ]); assert.equal(getValidatedModels().length, 1); - assert.equal(getRecommendedValidatedModel()?.key, "moonshotai/kimi-k2.6"); + assert.equal(getValidatedModels()[0]?.key, "moonshotai/kimi-k2.6"); + assert.equal(getRecommendedValidatedModel(), undefined); assert.equal(CONTRACT_METADATA.curatedRegistryPublished, true); for (const [key, model] of Object.entries(CURATED_MODEL_REGISTRY)) { assert.equal(model.adapterPackage, "@ai-sdk/openai-compatible"); assert.equal(model.transport, "chat_completions"); - if (key === "moonshotai/kimi-k2.6") { - assert.equal(model.validationStatus, "validated"); - assert.equal(model.recommended, true); - } else { - assert.equal(model.validationStatus, "candidate"); - assert.equal(model.recommended, false); - } + assert.equal( + model.validationStatus, + key === "moonshotai/kimi-k2.6" ? "validated" : "candidate", + ); + assert.equal( + model.recommended, + key === "deepseek-ai/deepseek-v4-flash-0731", + ); } + assert.equal( + CURATED_MODEL_REGISTRY["deepseek-ai/deepseek-v4-flash-0731"].modelId, + "deepseek-ai/deepseek-v4-flash-0731", + ); + assert.equal( + CURATED_MODEL_REGISTRY["deepseek-ai/deepseek-v4-flash-0731"].limits + ?.context, + 400_000, + ); assert.equal( CURATED_MODEL_REGISTRY["moonshotai/kimi-k2.6"].modelId, "moonshotai/kimi-k2.6", From ddc4b1407c673bc63d1197c5954f8df1f4988938 Mon Sep 17 00:00:00 2001 From: Daniil Koryto Date: Tue, 18 Aug 2026 15:21:50 +0300 Subject: [PATCH 2/3] fix(models): keep the curated recommendation on a validated model The first revision marked deepseek-ai/deepseek-v4-flash-0731 as the recommended curated entry while it is still validationStatus "candidate". Every consumer of the recommended concept filters on "validated" first (selection.ts, createCuratedModelIndex, managed-provider-config.ts), so the flag was inert: it did not make DeepSeek the curated default, it only removed the existing one. It also collapsed RecommendedValidatedCuratedModel to never and gave getRecommendedValidatedModel() the literal return type undefined. DeepSeek V4 Flash 0731 stays in the registry as a candidate. The curated recommendation returns to moonshotai/kimi-k2.6, the only entry with MiMoCode workflow proof. Public picker behavior is unaffected: it is built from the live GET /v1/models response, where DeepSeek is already first and therefore already the default. Co-Authored-By: Claude Opus 5 --- docs/model-validation.md | 12 ++++++++---- src/constants/models.ts | 4 ++-- test/package-contract.test.ts | 7 ++----- 3 files changed, 12 insertions(+), 11 deletions(-) diff --git a/docs/model-validation.md b/docs/model-validation.md index eb918a1..f8b19ba 100644 --- a/docs/model-validation.md +++ b/docs/model-validation.md @@ -18,10 +18,14 @@ The current public GonkaGate models page also lists: GonkaGate `/v1/models` availability is setup-catalog proof, not full MiMoCode workflow validation proof. -`deepseek-ai/deepseek-v4-flash-0731` is the recommended curated entry in -`src/constants/models.ts`, so it is the model the curated registry points new -setups at. It has no MiMoCode workflow proof yet, so it stays `candidate` and -the curated registry currently exposes no recommended validated model. +`deepseek-ai/deepseek-v4-flash-0731` is the first entry of the live GonkaGate +catalog, so `src/install/model-catalog.ts` already marks it recommended for the +public picker and it is what a fresh `npx @gonkagate/mimo-code-setup` run +defaults to. In the curated registry it stays `candidate` and +`recommended: false`, because no MiMoCode workflow proof exists for it yet and +only a `validated` entry may hold the curated recommendation. Promoting it +inside `src/constants/models.ts` requires a real MiMoCode validation run +recorded in `src/constants/model-validation.ts` first. Live MiMoCode validation for Kimi uses the full GonkaGate slug as the MiMoCode model key, so the effective model ref is diff --git a/src/constants/models.ts b/src/constants/models.ts index 83bb77b..e2916e0 100644 --- a/src/constants/models.ts +++ b/src/constants/models.ts @@ -101,7 +101,7 @@ export const CURATED_MODEL_REGISTRY = Object.freeze({ context: 400_000, }, modelId: "deepseek-ai/deepseek-v4-flash-0731", - recommended: true, + recommended: false, transport: "chat_completions", validationStatus: "candidate", }, @@ -112,7 +112,7 @@ export const CURATED_MODEL_REGISTRY = Object.freeze({ context: 262_000, }, modelId: "moonshotai/kimi-k2.6", - recommended: false, + recommended: true, transport: "chat_completions", validationStatus: "validated", }, diff --git a/test/package-contract.test.ts b/test/package-contract.test.ts index 83a9bdd..593a79c 100644 --- a/test/package-contract.test.ts +++ b/test/package-contract.test.ts @@ -127,7 +127,7 @@ test("curated model registry is present but not falsely validated", () => { ]); assert.equal(getValidatedModels().length, 1); assert.equal(getValidatedModels()[0]?.key, "moonshotai/kimi-k2.6"); - assert.equal(getRecommendedValidatedModel(), undefined); + assert.equal(getRecommendedValidatedModel()?.key, "moonshotai/kimi-k2.6"); assert.equal(CONTRACT_METADATA.curatedRegistryPublished, true); for (const [key, model] of Object.entries(CURATED_MODEL_REGISTRY)) { @@ -137,10 +137,7 @@ test("curated model registry is present but not falsely validated", () => { model.validationStatus, key === "moonshotai/kimi-k2.6" ? "validated" : "candidate", ); - assert.equal( - model.recommended, - key === "deepseek-ai/deepseek-v4-flash-0731", - ); + assert.equal(model.recommended, key === "moonshotai/kimi-k2.6"); } assert.equal( From 0d11b091972fdb0c9dc21cf5eee59c708a412cd7 Mon Sep 17 00:00:00 2001 From: Daniil Koryto Date: Tue, 18 Aug 2026 16:10:04 +0300 Subject: [PATCH 3/3] fix(models): read model names and context limits from the live catalog Delete the curated model registry, its pinned context windows, its display names, and the recommended-model flag. Model ids, names, descriptions, context windows, and the non-interactive default now come from GET /v1/models. Optional per-model fields degrade instead of failing, so a gateway that returns only model ids still completes setup and no longer gets a generic zero context limit written into MiMoCode config. Co-Authored-By: Claude Opus 5 --- AGENTS.md | 19 +- README.md | 12 +- docs/how-it-works.md | 37 +-- docs/model-validation.md | 37 ++- docs/runtime-contract-map.md | 7 +- docs/specs/mimo-code-setup-prd/spec.md | 38 ++- src/cli/contracts.ts | 4 +- src/cli/execute.ts | 4 +- src/constants/contract.ts | 3 +- src/constants/models.ts | 246 +++++-------------- src/install/contracts.ts | 5 +- src/install/contracts/install-state.ts | 4 +- src/install/deps.ts | 6 +- src/install/managed-config-mutations.ts | 9 +- src/install/managed-provider-config.ts | 49 ++-- src/install/model-catalog.ts | 60 ++++- src/install/scope.ts | 4 +- src/install/selection.ts | 45 ++-- src/install/session.ts | 9 +- test/cli.test.ts | 5 +- test/docs-contract.test.ts | 31 ++- test/install/managed-provider-config.test.ts | 39 ++- test/install/model-catalog.test.ts | 105 +++++++- test/install/rerun.test.ts | 6 +- test/install/scope.test.ts | 8 +- test/install/selection.test.ts | 50 ++-- test/install/session.test.ts | 84 ++++++- test/install/test-deps.ts | 11 +- test/install/verify-layers.test.ts | 1 - test/package-contract.test.ts | 92 ++----- 30 files changed, 578 insertions(+), 452 deletions(-) diff --git a/AGENTS.md b/AGENTS.md index 531f86b..ba1aaac 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -33,6 +33,12 @@ Current honest state: - the runtime collects the GonkaGate key before the model picker, calls `GET https://api.gonkagate.com/v1/models`, and writes every returned model into `provider.gonkagate.models` +- model ids, display names, descriptions, context windows, and the default + model all come from that live response; the repository carries no model + catalog and no default model id +- `name`, `description`, and `context_length` are optional on the wire, so a + gateway that returns only `id`, `object`, `created`, and `owned_by` still + completes setup - `moonshotai/kimi-k2.6` has MiMoCode validation proof for the current minimum-supported MiMoCode contract, but the public picker is now backed by the live GonkaGate model catalog rather than a hardcoded validated allowlist @@ -96,6 +102,13 @@ change. - project config must not own the secret binding - public setup must fetch the model catalog after safe API-key intake instead of exposing a hardcoded model allowlist +- model ids, display names, and context windows must never be checked in; they + are read from the live `/v1/models` response +- the non-interactive default model is `data[0]` of the live response, in + response order; no client-side ranking, sorting, or preference heuristic +- optional live metadata must degrade instead of failing: a missing `name` + falls back to the model id, a missing `description` is omitted, and a missing + `context_length` writes no MiMoCode `limit` block rather than a `0` limit - installer success must be based on effective MiMoCode config, not only file writes - raw `mimo --pure debug config` output must not be printed because `{file:...}` @@ -212,11 +225,15 @@ results. The default public flow fetches the GonkaGate model catalog from ### `src/install/model-catalog.ts` Trust-boundary parser and fetch adapter for `GET /v1/models`. Keep it strict -about response shape and redaction-safe about failures. +about the required response shape, tolerant about optional per-model metadata, +and redaction-safe about failures. ### `src/constants/` Package, provider, transport, path, and model-validation constants. +`src/constants/models.ts` holds runtime model types only; model data belongs to +the live catalog. `src/constants/model-validation.ts` is the MiMoCode workflow +proof ledger and must never record a validation that did not happen. ### `.agents/skills/` and `.claude/skills/` diff --git a/README.md b/README.md index d3a6292..c9f0d40 100644 --- a/README.md +++ b/README.md @@ -59,7 +59,8 @@ The happy path is: 1. The CLI checks that `mimo` is installed and supported. 2. It asks for your GonkaGate API key in a masked prompt. -3. It calls `GET /v1/models` and offers every model returned by GonkaGate. +3. It calls `GET /v1/models` and offers every model returned by GonkaGate, + labelled with the live model name and description. 4. It asks whether GonkaGate should be activated for `user` or `project` scope. 5. It writes the managed config, verifies the result, and tells you to go back @@ -89,6 +90,9 @@ If you run non-interactively, pass `--scope` or `--yes`. In a git repository, the recommended default is usually `project`; outside a repo, it is usually `user`. +Without `--model`, `--yes` selects the first model of the live `/v1/models` +response. GonkaGate owns that order, so the CLI ships no default model id. + ## Before You Run It You need: @@ -182,13 +186,17 @@ The runtime is live-catalog-first: - the canonical base URL is `https://api.gonkagate.com/v1` - the setup model list is fetched from `https://api.gonkagate.com/v1/models` after safe API-key intake +- model ids, display names, and context windows are read from that live + response; this repository ships no model catalog and no default model id +- gateways that do not publish per-model metadata still work: the model id is + used as the display name and no context limit is written - the current provider package is `@ai-sdk/openai-compatible` - the current transport target is `chat_completions` - future migration should add `responses` support without renaming the product - the selected setup model remains the activation default through `model` and `small_model` - `provider.gonkagate.models` is generated from every model returned by - `/v1/models` + `/v1/models`, including the published context window when there is one - `docs/model-validation.md` tracks MiMoCode workflow proof separately from live catalog availability diff --git a/docs/how-it-works.md b/docs/how-it-works.md index cda3f53..fea0372 100644 --- a/docs/how-it-works.md +++ b/docs/how-it-works.md @@ -15,7 +15,9 @@ a hardcoded allowlist. 3. Collect a GonkaGate API key through safe inputs only: `GONKAGATE_API_KEY`, masked interactive prompt, or `--api-key-stdin`. 4. Fetch `https://api.gonkagate.com/v1/models` with Bearer auth and build the - setup picker from every returned model id. + setup picker from every returned model id, using the live `name`, + `description`, and `context_length` when the gateway publishes them. The + non-interactive default is the first model of the response. 5. Store the secret under `~/.gonkagate/mimo-code/api-key`. 6. Write user-level provider config for `provider.gonkagate`. 7. Write only activation settings for project scope. @@ -64,26 +66,15 @@ The intended managed provider shape is: "setCacheKey": false }, "models": { - "deepseek-ai/deepseek-v4-flash-0731": { - "name": "deepseek-ai/deepseek-v4-flash-0731", + "": { + "name": "", "limit": { - "context": 0, + "context": "", "output": 0 } }, - "moonshotai/kimi-k2.6": { - "name": "moonshotai/kimi-k2.6", - "limit": { - "context": 0, - "output": 0 - } - }, - "minimaxai/minimax-m2.7": { - "name": "minimaxai/minimax-m2.7", - "limit": { - "context": 0, - "output": 0 - } + "": { + "name": "" } } } @@ -93,7 +84,17 @@ The intended managed provider shape is: The concrete `models` object is generated from the live `/v1/models` response. The API model ids are also the MiMoCode model keys under -`provider.gonkagate.models`. +`provider.gonkagate.models`. This repository keeps no checked-in model catalog: +ids, display names, and context windows are read from the live response on +every run. + +Per-model metadata is optional on the wire, because a GonkaGate gateway may +still return only `id`, `object`, `created`, and `owned_by`: + +- missing `name` falls back to the model id +- missing `description` is omitted from the picker +- missing `context_length` writes no `limit` block for that model, so MiMoCode + keeps its own default instead of being told the context window is `0` `setCacheKey` is disabled because live GonkaGate chat-completions requests reject the non-standard `promptCacheKey` parameter emitted by the AI SDK when diff --git a/docs/model-validation.md b/docs/model-validation.md index f8b19ba..aa063cd 100644 --- a/docs/model-validation.md +++ b/docs/model-validation.md @@ -6,27 +6,17 @@ the MiMoCode workflow proof ledger. Current MiMoCode-validated workflow proof exists for: -- `moonshotai/kimi-k2.6` - Kimi K2.6, 240K context. +- `moonshotai/kimi-k2.6`, recorded in `src/constants/model-validation.ts`. -The current public GonkaGate models page also lists: - -- `deepseek-ai/deepseek-v4-flash-0731` - DeepSeek V4 Flash 0731, 400K context. -- `minimaxai/minimax-m2.7` - MiniMax M2.7, 180K context. -- `qwen/qwen3-235b-a22b-instruct-2507-fp8` - Qwen3 235B A22B - Instruct 2507 FP8, 240K context. +Which models exist, what they are called, and how large their context windows +are is owned by the live `GET /v1/models` response. This repository keeps no +copy of that catalog, so nothing here has to be edited when GonkaGate adds, +renames, or retires a model. A model is named in this document because +MiMoCode workflow proof exists for it, not because it is available. GonkaGate `/v1/models` availability is setup-catalog proof, not full MiMoCode workflow validation proof. -`deepseek-ai/deepseek-v4-flash-0731` is the first entry of the live GonkaGate -catalog, so `src/install/model-catalog.ts` already marks it recommended for the -public picker and it is what a fresh `npx @gonkagate/mimo-code-setup` run -defaults to. In the curated registry it stays `candidate` and -`recommended: false`, because no MiMoCode workflow proof exists for it yet and -only a `validated` entry may hold the curated recommendation. Promoting it -inside `src/constants/models.ts` requires a real MiMoCode validation run -recorded in `src/constants/model-validation.ts` first. - Live MiMoCode validation for Kimi uses the full GonkaGate slug as the MiMoCode model key, so the effective model ref is `gonkagate/moonshotai/kimi-k2.6`. Short aliases such as @@ -61,11 +51,14 @@ must prove: - docs and tests name the model as MiMoCode workflow-validated only after the proof exists -The registry types already allow transport, adapter package, provider options, -model options, model headers, limits, and migration metadata so MiMoCode-specific -requirements can be added without changing the public shape later. +The runtime model types in `src/constants/models.ts` already allow transport, +adapter package, provider options, model options, model headers, limits, and +migration metadata so MiMoCode-specific requirements can be added without +changing the public shape later. Those types carry no model data; every value +is filled in from the live catalog at setup time. Validation records are represented in `src/constants/model-validation.ts`. -Contract tests reject any registry entry marked `validated` without a matching -record. Additional live GonkaGate workflow proof is a gated validation activity -and is not part of default CI. +Contract tests check that each record is internally consistent with the +provider package and transport this installer writes. A record may only be +added after the proof above actually exists; live GonkaGate workflow proof is a +gated validation activity and is not part of default CI. diff --git a/docs/runtime-contract-map.md b/docs/runtime-contract-map.md index d0af337..52d5a29 100644 --- a/docs/runtime-contract-map.md +++ b/docs/runtime-contract-map.md @@ -24,9 +24,10 @@ update these surfaces together: - `CHANGELOG.md` - meaningful user-facing runtime changes. - `src/constants/contract.ts` - package identity, public implementation status, MiMoCode minimum version, and live catalog source. -- `src/install/model-catalog.ts` - `/v1/models` fetch and response-shape - boundary. -- `src/constants/models.ts` - model metadata and validation helper types. +- `src/install/model-catalog.ts` - `/v1/models` fetch, response-shape boundary, + and the fallbacks for gateways that publish no per-model metadata. +- `src/constants/models.ts` - runtime model shape types only; it must not carry + a checked-in catalog, context window, display name, or default model id. - `test/docs-contract.test.ts` and `test/package-contract.test.ts` - docs, constants, package metadata, and model registry agreement. - `test/cli.test.ts` - human and JSON CLI output semantics. diff --git a/docs/specs/mimo-code-setup-prd/spec.md b/docs/specs/mimo-code-setup-prd/spec.md index 963432c..a591b98 100644 --- a/docs/specs/mimo-code-setup-prd/spec.md +++ b/docs/specs/mimo-code-setup-prd/spec.md @@ -418,9 +418,9 @@ the actual `models` entries generated from GonkaGate `/v1/models`: }, "models": { "/": { - "name": "", + "name": "", "limit": { - "context": 0, + "context": "", "output": 0 } } @@ -430,6 +430,11 @@ the actual `models` entries generated from GonkaGate `/v1/models`: } ``` +`name` and `limit` are generated from the live `/v1/models` entry, never from +a checked-in catalog. When the gateway does not publish `context_length` for a +model, the installer writes no `limit` block for it at all and MiMoCode keeps +its own default. Writing `"context": 0` would state a real but wrong limit. + The installer should not write `provider.gonkagate.env` as a durable runtime dependency in v1. `GONKAGATE_API_KEY` is setup input, not the normal post-setup runtime path. @@ -456,13 +461,26 @@ Runtime model source: - endpoint: `GET https://api.gonkagate.com/v1/models` - auth: `Authorization: Bearer ` -- expected response shape: `{ "object": "list", "data": [{ "id": "..."}] }` +- required response shape: `{ "object": "list", "data": [{ "id": "..."}] }` - setup keys: every returned `data[].id` +- optional per-model metadata: `name`, `description`, `context_length` + +The installer must not hardcode the public setup model list, model display +names, or model context windows. It should parse the authenticated +`/v1/models` response, preserve the returned order, dedupe duplicate ids, and +write every returned model id under `provider.gonkagate.models`. + +Optional metadata is a compatibility surface, not a requirement. A gateway that +publishes only `id`, `object`, `created`, and `owned_by` must still complete +setup, so each optional field may be absent, `null`, or unusable: + +- `name` absent -> the model id is the display name +- `description` absent -> no description is shown in the picker +- `context_length` absent -> no `limit` block is written for that model -The installer must not hardcode the public setup model list. It should parse -the authenticated `/v1/models` response, preserve the returned order, dedupe -duplicate ids, and write every returned model id under -`provider.gonkagate.models`. +The default model for non-interactive setup is `data[0]`, the first entry of +the live response in response order. The installer must not rank, sort, or +prefer models on its own, and must not carry a checked-in default model id. Model ids must map cleanly to MiMoCode's `provider/model` model-ref format. Because MiMoCode treats the first slash segment as provider id and rejoins the @@ -729,8 +747,8 @@ The setup tool must not depend on a future `gonkagate doctor`. 7. Future responses migration must not require a new package identity. 8. Interactive setup should fetch the live model catalog before showing the model picker. -9. Safe non-interactive setup may accept recommended defaults only when the - installer has enough information to do so without ambiguity. +9. Safe non-interactive setup selects the first model of the live `/v1/models` + response, so the gateway owns the default instead of the installer. 10. Diagnostics must be actionable without exposing secrets. 11. Validation proof must be narrow enough to run locally but broad enough to cover the claimed MiMoCode behavior. @@ -741,7 +759,7 @@ The setup tool must not depend on a future `gonkagate doctor`. - native MiMoCode `auth.json` integration, if a later product decision chooses to use it - richer post-setup live GonkaGate session verification -- richer live catalog metadata, if `/v1/models` starts returning it +- live catalog metadata beyond `name`, `description`, and `context_length` - cheaper validated `small_model` strategy - MiMoCode model-group integration - future `/v1/responses` migration diff --git a/src/cli/contracts.ts b/src/cli/contracts.ts index 27f357a..08ecd95 100644 --- a/src/cli/contracts.ts +++ b/src/cli/contracts.ts @@ -1,4 +1,4 @@ -import type { CuratedModelRegistry } from "../constants/models.js"; +import type { ModelRegistry } from "../constants/models.js"; import type { InstallerDeps } from "../install/deps.js"; export interface CliOptions { @@ -17,7 +17,7 @@ export interface CliRunResult { export interface CliRunOptions { deps?: InstallerDeps; - registry?: CuratedModelRegistry; + registry?: ModelRegistry; stderr?: Pick; stdout?: Pick; } diff --git a/src/cli/execute.ts b/src/cli/execute.ts index c8d1df1..83f227c 100644 --- a/src/cli/execute.ts +++ b/src/cli/execute.ts @@ -1,5 +1,5 @@ import type { CliOptions, CliRunResult } from "./contracts.js"; -import type { CuratedModelRegistry } from "../constants/models.js"; +import type { ModelRegistry } from "../constants/models.js"; import type { InstallerDeps } from "../install/deps.js"; import { runInstaller } from "../install/index.js"; import { renderInstallerJson, renderInstallerText } from "./render.js"; @@ -8,7 +8,7 @@ export async function executeCli( parsedOptions: CliOptions, streams: { stdout: Pick }, deps?: InstallerDeps, - registry?: CuratedModelRegistry, + registry?: ModelRegistry, ): Promise { const result = await runInstaller( { diff --git a/src/constants/contract.ts b/src/constants/contract.ts index b91ca92..cd6d7fc 100644 --- a/src/constants/contract.ts +++ b/src/constants/contract.ts @@ -3,11 +3,10 @@ export const CONTRACT_METADATA = { legacyBinName: "gonkagate-mimo-code", binPath: "bin/gonkagate-mimo-code.js", cliVersion: "0.3.3", // x-release-please-version - curatedRegistryPublished: true, packageName: "@gonkagate/mimo-code-setup", publicEntrypoint: "npx @gonkagate/mimo-code-setup", publicState: - "Installer runtime fetches the available GonkaGate model catalog from /v1/models after safe API-key intake.", + "Installer runtime fetches the available GonkaGate models and their metadata from /v1/models after safe API-key intake.", mimoCode: { minVersion: "0.1.0", packageName: "@mimo-ai/cli", diff --git a/src/constants/models.ts b/src/constants/models.ts index e2916e0..dcceecf 100644 --- a/src/constants/models.ts +++ b/src/constants/models.ts @@ -1,148 +1,91 @@ -export const CURATED_MODEL_TRANSPORTS = Object.freeze([ +export const MODEL_TRANSPORTS = Object.freeze([ "chat_completions", "responses", ] as const); -export type CuratedModelTransport = (typeof CURATED_MODEL_TRANSPORTS)[number]; -export type CuratedModelValidationStatus = "candidate" | "validated"; +export type ModelTransport = (typeof MODEL_TRANSPORTS)[number]; +export type ModelValidationStatus = "candidate" | "validated"; -export interface CuratedModelProviderOverride { - api?: CuratedModelTransport; +export interface ModelProviderOverride { + api?: ModelTransport; npm?: string; } -export interface CuratedModelCompatibility { +export interface ModelCompatibility { modelHeaders?: Readonly>; modelOptions?: Readonly>; - modelProvider?: Readonly; + modelProvider?: Readonly; notes?: readonly string[]; providerOptions?: Readonly>; } -export interface CuratedModelLimits { +export interface ModelLimits { context?: number; output?: number; } -export interface CuratedModelMigrationMetadata { +export interface ModelMigrationMetadata { adapterPackage?: string; - transport?: CuratedModelTransport; + transport?: ModelTransport; } -export interface CuratedModelDefinition { +/** + * Runtime shape of one GonkaGate model. + * + * Every field except the installer-owned adapter/transport metadata comes from + * the live `GET /v1/models` response. This repository does not check in a model + * catalog, model ids, display names, or context windows. + */ +export interface ModelDefinition { adapterPackage: string; + description?: string; displayName: string; - limits?: CuratedModelLimits; - migrationMetadata?: CuratedModelMigrationMetadata; + limits?: ModelLimits; + migrationMetadata?: ModelMigrationMetadata; modelId: string; - recommended: boolean; - runtimeCompatibility?: CuratedModelCompatibility; - transport: CuratedModelTransport; - validationStatus: CuratedModelValidationStatus; + runtimeCompatibility?: ModelCompatibility; + transport: ModelTransport; + validationStatus: ModelValidationStatus; } -export interface CuratedModelRegistry { - readonly [key: string]: CuratedModelDefinition; +export interface ModelRegistry { + readonly [key: string]: ModelDefinition; } -export type CuratedModelRecord = - CuratedModelDefinition & { - key: TKey; - }; +export type ModelRecord = ModelDefinition & { + key: TKey; +}; -type CuratedModelKeyOf = Extract< +type ModelKeyOf = Extract< keyof TRegistry, string >; -type CuratedModelRecordFor< - TRegistry extends CuratedModelRegistry, - TKey extends CuratedModelKeyOf = CuratedModelKeyOf, +type ModelRecordFor< + TRegistry extends ModelRegistry, + TKey extends ModelKeyOf = ModelKeyOf, > = TRegistry[TKey] & { key: TKey; }; -type ValidatedCuratedModelRecordFor< - TRegistry extends CuratedModelRegistry, - TKey extends CuratedModelKeyOf = CuratedModelKeyOf, -> = Extract< - CuratedModelRecordFor, - { validationStatus: "validated" } ->; +type ValidatedModelRecordFor< + TRegistry extends ModelRegistry, + TKey extends ModelKeyOf = ModelKeyOf, +> = Extract, { validationStatus: "validated" }>; -type RecommendedValidatedCuratedModelRecordFor< - TRegistry extends CuratedModelRegistry, - TKey extends CuratedModelKeyOf = CuratedModelKeyOf, -> = Extract< - ValidatedCuratedModelRecordFor, - { recommended: true } ->; - -export interface CuratedModelIndex< - TRegistry extends CuratedModelRegistry = CuratedModelRegistry, -> { - modelKeys: readonly CuratedModelKeyOf[]; - models: readonly CuratedModelRecordFor[]; - recommendedValidatedModel: - | RecommendedValidatedCuratedModelRecordFor - | undefined; - validatedModelKeys: readonly ValidatedCuratedModelRecordFor["key"][]; - validatedModels: readonly ValidatedCuratedModelRecordFor[]; +export interface ModelIndex { + modelKeys: readonly ModelKeyOf[]; + models: readonly ModelRecordFor[]; + validatedModelKeys: readonly ValidatedModelRecordFor["key"][]; + validatedModels: readonly ValidatedModelRecordFor[]; } export type MimoCodeModelRef = `gonkagate/${TKey}`; -export const CURATED_MODEL_REGISTRY = Object.freeze({ - "deepseek-ai/deepseek-v4-flash-0731": { - adapterPackage: "@ai-sdk/openai-compatible", - displayName: "DeepSeek V4 Flash 0731", - limits: { - context: 400_000, - }, - modelId: "deepseek-ai/deepseek-v4-flash-0731", - recommended: false, - transport: "chat_completions", - validationStatus: "candidate", - }, - "moonshotai/kimi-k2.6": { - adapterPackage: "@ai-sdk/openai-compatible", - displayName: "Kimi K2.6", - limits: { - context: 262_000, - }, - modelId: "moonshotai/kimi-k2.6", - recommended: true, - transport: "chat_completions", - validationStatus: "validated", - }, - "minimaxai/minimax-m2.7": { - adapterPackage: "@ai-sdk/openai-compatible", - displayName: "MiniMax M2.7", - limits: { - context: 205_000, - }, - modelId: "minimaxai/minimax-m2.7", - recommended: false, - transport: "chat_completions", - validationStatus: "candidate", - }, - "qwen/qwen3-235b-a22b-instruct-2507-fp8": { - adapterPackage: "@ai-sdk/openai-compatible", - displayName: "Qwen3 235B A22B Instruct 2507 FP8", - limits: { - context: 262_000, - }, - modelId: "qwen/qwen3-235b-a22b-instruct-2507-fp8", - recommended: false, - transport: "chat_completions", - validationStatus: "candidate", - }, -} as const satisfies CuratedModelRegistry); - -function toCuratedModelRecord< +function toModelRecord< TKey extends string, - TDefinition extends CuratedModelDefinition, + TDefinition extends ModelDefinition, >(key: TKey, definition: TDefinition): TDefinition & { key: TKey } { return { ...definition, @@ -151,34 +94,36 @@ function toCuratedModelRecord< } export function isValidatedModel< - TModel extends { validationStatus: CuratedModelValidationStatus }, + TModel extends { validationStatus: ModelValidationStatus }, >(model: TModel): model is Extract { return model.validationStatus === "validated"; } -export function isRecommendedCuratedModel< - TModel extends { recommended: boolean }, ->(model: TModel): model is Extract { - return model.recommended; +export function isModelTransport(value: unknown): value is ModelTransport { + return ( + typeof value === "string" && + MODEL_TRANSPORTS.includes(value as ModelTransport) + ); } -export function createCuratedModelIndex( +/** + * Index a registry while preserving live catalog order. Order is meaningful: + * the first entry returned by `GET /v1/models` is the setup default. + */ +export function createModelIndex( registry: TRegistry, -): CuratedModelIndex { - type RegistryKey = CuratedModelKeyOf; - type RegistryModel = CuratedModelRecordFor; - type ValidatedRegistryModel = ValidatedCuratedModelRecordFor; - type RecommendedValidatedRegistryModel = - RecommendedValidatedCuratedModelRecordFor; +): ModelIndex { + type RegistryKey = ModelKeyOf; + type RegistryModel = ModelRecordFor; + type ValidatedRegistryModel = ValidatedModelRecordFor; const modelKeys = Object.keys(registry) as RegistryKey[]; const models: RegistryModel[] = []; const validatedModels: ValidatedRegistryModel[] = []; const validatedModelKeys: ValidatedRegistryModel["key"][] = []; - const recommendedValidatedModels: RecommendedValidatedRegistryModel[] = []; for (const key of modelKeys) { - const model = toCuratedModelRecord(key, registry[key]); + const model = toModelRecord(key, registry[key]); models.push(model); if (!isValidatedModel(model)) { @@ -187,87 +132,16 @@ export function createCuratedModelIndex( validatedModels.push(model); validatedModelKeys.push(model.key); - - if (isRecommendedCuratedModel(model)) { - recommendedValidatedModels.push(model); - } - } - - if (recommendedValidatedModels.length > 1) { - throw new Error( - "Curated model registry must not expose more than one recommended validated model.", - ); } return { modelKeys: Object.freeze(modelKeys), models: Object.freeze(models), - recommendedValidatedModel: recommendedValidatedModels[0], validatedModelKeys: Object.freeze(validatedModelKeys), validatedModels: Object.freeze(validatedModels), }; } -type DefaultCuratedModelRegistry = typeof CURATED_MODEL_REGISTRY; - -export type CuratedModelKey = CuratedModelKeyOf; -export type CuratedModel = CuratedModelRecordFor; -export type CuratedModelByKey = - CuratedModelRecordFor; -export type ValidatedCuratedModel = - ValidatedCuratedModelRecordFor; -export type RecommendedValidatedCuratedModel = - RecommendedValidatedCuratedModelRecordFor; - -const DEFAULT_CURATED_MODEL_INDEX = createCuratedModelIndex( - CURATED_MODEL_REGISTRY, -); - -export const SUPPORTED_MODELS: readonly CuratedModel[] = - DEFAULT_CURATED_MODEL_INDEX.models; - -export const SUPPORTED_MODEL_KEYS: readonly CuratedModelKey[] = - DEFAULT_CURATED_MODEL_INDEX.modelKeys; - -export function isCuratedModelKey(key: string): key is CuratedModelKey { - return key in CURATED_MODEL_REGISTRY; -} - -export function isCuratedModelTransport( - value: unknown, -): value is CuratedModelTransport { - return ( - typeof value === "string" && - CURATED_MODEL_TRANSPORTS.includes(value as CuratedModelTransport) - ); -} - -export function getCuratedModelByKey( - key: TKey, -): CuratedModelByKey; -export function getCuratedModelByKey(key: string): CuratedModel | undefined; -export function getCuratedModelByKey(key: string): CuratedModel | undefined { - if (!isCuratedModelKey(key)) { - return undefined; - } - - return toCuratedModelRecord(key, CURATED_MODEL_REGISTRY[key]); -} - -export function getValidatedModels(): readonly ValidatedCuratedModel[] { - return DEFAULT_CURATED_MODEL_INDEX.validatedModels; -} - -export function getValidatedModelKeys(): readonly ValidatedCuratedModel["key"][] { - return DEFAULT_CURATED_MODEL_INDEX.validatedModelKeys; -} - -export function getRecommendedValidatedModel(): - | RecommendedValidatedCuratedModel - | undefined { - return DEFAULT_CURATED_MODEL_INDEX.recommendedValidatedModel; -} - export function formatMimoCodeModelRef( model: TKey | { key: TKey }, ): MimoCodeModelRef { diff --git a/src/install/contracts.ts b/src/install/contracts.ts index 1d6a0e4..69a2180 100644 --- a/src/install/contracts.ts +++ b/src/install/contracts.ts @@ -1,4 +1,4 @@ -import type { CuratedModelTransport } from "../constants/models.js"; +import type { ModelTransport } from "../constants/models.js"; export type InstallScope = "user" | "project"; export type InstallerStatus = "success" | "blocked" | "failed"; @@ -48,7 +48,7 @@ export interface InstallerSuccessResult { modelRef: `gonkagate/${string}`; scope: InstallScope; provider: "gonkagate"; - transport: CuratedModelTransport; + transport: ModelTransport; mimoCode: MimoCodeVersionInfo; configTargets: ConfigTargets; verification: VerificationSummary; @@ -124,7 +124,6 @@ export type InstallerErrorCode = | "model_catalog_empty" | "validated_models_unavailable" | "unsupported_model" - | "ambiguous_model_selection" | "unexpected_error"; export interface InstallerErrorShape { diff --git a/src/install/contracts/install-state.ts b/src/install/contracts/install-state.ts index dcd8a42..667d1d2 100644 --- a/src/install/contracts/install-state.ts +++ b/src/install/contracts/install-state.ts @@ -1,5 +1,5 @@ import type { InstallScope } from "../contracts.js"; -import type { CuratedModelTransport } from "../../constants/models.js"; +import type { ModelTransport } from "../../constants/models.js"; export interface InstallState { globalConfigTarget: string; @@ -12,5 +12,5 @@ export interface InstallState { providerPackage: string; scope: InstallScope; selectedModelKey: string; - transport: CuratedModelTransport; + transport: ModelTransport; } diff --git a/src/install/deps.ts b/src/install/deps.ts index 64e1391..d5d288c 100644 --- a/src/install/deps.ts +++ b/src/install/deps.ts @@ -84,7 +84,11 @@ export interface PromptAdapter { password(message: string, options?: PasswordPromptOptions): Promise; select( message: string, - choices: readonly { name: string; value: TValue }[], + choices: readonly { + description?: string; + name: string; + value: TValue; + }[], ): Promise; } diff --git a/src/install/managed-config-mutations.ts b/src/install/managed-config-mutations.ts index e12703f..31dd650 100644 --- a/src/install/managed-config-mutations.ts +++ b/src/install/managed-config-mutations.ts @@ -1,7 +1,6 @@ import { - CURATED_MODEL_REGISTRY, formatMimoCodeModelRef, - type CuratedModelRegistry, + type ModelRegistry, } from "../constants/models.js"; import type { InstallState } from "./contracts/install-state.js"; import { getConfigValue } from "./config-value.js"; @@ -10,7 +9,8 @@ import { deleteJsoncValue, parseJsoncDocument } from "./jsonc.js"; export interface CleanupActivationOptions { currentModelKey: string; installState?: InstallState; - registry?: CuratedModelRegistry; + /** Live catalog for this run; absent when no catalog was fetched. */ + registry?: ModelRegistry; } export function cleanupInstallerOwnedActivation( @@ -42,8 +42,7 @@ export function isInstallerOwnedModelRef( return true; } - const registry = options.registry ?? CURATED_MODEL_REGISTRY; - return Object.keys(registry).some( + return Object.keys(options.registry ?? {}).some( (key) => value === formatMimoCodeModelRef(key), ); } diff --git a/src/install/managed-provider-config.ts b/src/install/managed-provider-config.ts index 5810189..e90a1ed 100644 --- a/src/install/managed-provider-config.ts +++ b/src/install/managed-provider-config.ts @@ -4,10 +4,10 @@ import { GONKAGATE_PROVIDER_ID, MANAGED_SECRET_FILE_REF, } from "../constants/gateway.js"; -import { - CURATED_MODEL_REGISTRY, - type CuratedModelDefinition, - type CuratedModelRegistry, +import type { + ModelDefinition, + ModelLimits, + ModelRegistry, } from "../constants/models.js"; export interface ManagedProviderConfig { @@ -22,7 +22,7 @@ export interface ManagedProviderConfig { } export interface ManagedProviderModelConfig { - limit: { + limit?: { context: number; output: number; }; @@ -32,24 +32,22 @@ export interface ManagedProviderModelConfig { } export function createManagedProviderConfig( - registry: CuratedModelRegistry = CURATED_MODEL_REGISTRY, + registry: ModelRegistry, ): ManagedProviderConfig { const models: Record = {}; for (const [key, model] of Object.entries(registry) as [ string, - CuratedModelDefinition, + ModelDefinition, ][]) { if (model.validationStatus !== "validated") { continue; } assertNoCanonicalOverride(model); + const limit = toManagedLimit(model.limits); models[key] = { - limit: { - context: model.limits?.context ?? 0, - output: model.limits?.output ?? 0, - }, + ...(limit === undefined ? {} : { limit }), name: model.displayName, ...(model.runtimeCompatibility?.modelHeaders === undefined ? {} @@ -72,7 +70,30 @@ export function createManagedProviderConfig( }; } -function assertNoCanonicalOverride(model: CuratedModelDefinition): void { +/** + * Build the MiMoCode `limit` block from what the gateway actually published. + * + * A gateway that has not shipped per-model metadata publishes no context + * window, so no `limit` block is written at all and MiMoCode keeps its own + * default. Writing `context: 0` would claim a real, wrong limit. + * + * When any limit is known, both keys are written because that is the shape + * proven against MiMoCode in `src/constants/model-validation.ts`. + */ +function toManagedLimit( + limits: ModelLimits | undefined, +): { context: number; output: number } | undefined { + if (limits?.context === undefined && limits?.output === undefined) { + return undefined; + } + + return { + context: limits.context ?? 0, + output: limits.output ?? 0, + }; +} + +function assertNoCanonicalOverride(model: ModelDefinition): void { const providerOptions = model.runtimeCompatibility?.providerOptions; if (providerOptions === undefined) { return; @@ -85,9 +106,7 @@ function assertNoCanonicalOverride(model: CuratedModelDefinition): void { } } -export function createManagedProviderConfigPatch( - registry?: CuratedModelRegistry, -) { +export function createManagedProviderConfigPatch(registry: ModelRegistry) { return { path: ["provider", GONKAGATE_PROVIDER_ID], value: createManagedProviderConfig(registry), diff --git a/src/install/model-catalog.ts b/src/install/model-catalog.ts index 97f205f..60a4365 100644 --- a/src/install/model-catalog.ts +++ b/src/install/model-catalog.ts @@ -3,19 +3,24 @@ import { CURRENT_TRANSPORT, GONKAGATE_BASE_URL, } from "../constants/gateway.js"; -import type { - CuratedModelDefinition, - CuratedModelRegistry, -} from "../constants/models.js"; +import type { ModelDefinition, ModelRegistry } from "../constants/models.js"; import type { InstallerDeps } from "./deps.js"; import { InstallerError } from "./errors.js"; export const GONKAGATE_MODELS_URL = `${GONKAGATE_BASE_URL}/models` as const; +/** + * Optional per-model metadata keys, in lookup order. + * + * Older gateways return only `id`, `object`, `created`, and `owned_by`, so + * every key below must stay optional and may also be `null`. + */ +const CONTEXT_LENGTH_KEYS = ["context_length", "contextLength"] as const; + export async function fetchGonkaGateModelCatalog( deps: InstallerDeps, apiKey: string, -): Promise { +): Promise { let response: Awaited>; try { @@ -53,9 +58,7 @@ export async function fetchGonkaGateModelCatalog( return parseGonkaGateModelCatalog(response.body); } -export function parseGonkaGateModelCatalog( - body: unknown, -): CuratedModelRegistry { +export function parseGonkaGateModelCatalog(body: unknown): ModelRegistry { if (!isRecord(body) || body.object !== "list" || !Array.isArray(body.data)) { throw new InstallerError({ category: "model_registry", @@ -64,7 +67,7 @@ export function parseGonkaGateModelCatalog( }); } - const registry: Record = {}; + const registry: Record = {}; for (const item of body.data) { if (!isRecord(item) || typeof item.id !== "string") { @@ -88,11 +91,17 @@ export function parseGonkaGateModelCatalog( continue; } + const description = readOptionalText(item.description); + const contextLength = readContextLength(item); + registry[modelId] = { adapterPackage: CURRENT_PROVIDER_PACKAGE, - displayName: modelId, + ...(description === undefined ? {} : { description }), + displayName: readOptionalText(item.name) ?? modelId, + ...(contextLength === undefined + ? {} + : { limits: { context: contextLength } }), modelId, - recommended: Object.keys(registry).length === 0, transport: CURRENT_TRANSPORT, validationStatus: "validated", }; @@ -109,6 +118,35 @@ export function parseGonkaGateModelCatalog( return registry; } +/** + * Read an optional catalog string. Absent, `null`, wrong-typed, and blank + * values all mean "the gateway did not publish this", never a hard failure: + * the caller falls back to what the installer already knows. + */ +function readOptionalText(value: unknown): string | undefined { + if (typeof value !== "string") { + return undefined; + } + + const trimmed = value.trim(); + return trimmed.length === 0 ? undefined : trimmed; +} + +/** + * Read the published context window. Returns `undefined` when the gateway does + * not publish one so no synthetic limit is written into MiMoCode config. + */ +function readContextLength(item: Record): number | undefined { + for (const key of CONTEXT_LENGTH_KEYS) { + const value = item[key]; + if (typeof value === "number" && Number.isInteger(value) && value > 0) { + return value; + } + } + + return undefined; +} + function isGonkaGateModelId(value: string): boolean { return ( value.length > 0 && diff --git a/src/install/scope.ts b/src/install/scope.ts index 178d30d..9ba0f98 100644 --- a/src/install/scope.ts +++ b/src/install/scope.ts @@ -1,7 +1,7 @@ import { GONKAGATE_PROVIDER_ID } from "../constants/gateway.js"; import { formatMimoCodeModelRef, - type CuratedModelRegistry, + type ModelRegistry, } from "../constants/models.js"; import type { InstallScope } from "./contracts.js"; import { applyManagedConfigValues } from "./config.js"; @@ -20,7 +20,7 @@ export interface ManagedConfigValue { export function createScopeWritePlan(input: { modelKey: string; - registry?: CuratedModelRegistry; + registry: ModelRegistry; scope: InstallScope; }): ScopeWritePlan { const modelRef = formatMimoCodeModelRef(input.modelKey); diff --git a/src/install/selection.ts b/src/install/selection.ts index c11c380..4da4b65 100644 --- a/src/install/selection.ts +++ b/src/install/selection.ts @@ -1,7 +1,7 @@ import { - type CuratedModelDefinition, - type CuratedModelRecord, - type CuratedModelRegistry, + type ModelDefinition, + type ModelRecord, + type ModelRegistry, } from "../constants/models.js"; import type { InstallerDeps } from "./deps.js"; import { InstallerError } from "./errors.js"; @@ -12,18 +12,22 @@ export interface ModelSelectionRequest { } export interface ModelSelection { - model: CuratedModelRecord; + model: ModelRecord; } +/** + * Pick the setup model from the live catalog. + * + * The non-interactive default is the first model of the live `/v1/models` + * response, in response order. The gateway owns that order; the installer must + * not rank, sort, or prefer models on its own. + */ export async function selectValidatedModel( request: ModelSelectionRequest, deps: InstallerDeps, - registry: CuratedModelRegistry, + registry: ModelRegistry, ): Promise { const validatedModels = getValidatedModelRecords(registry); - const recommendedModels = validatedModels.filter( - (model) => model.recommended, - ); if (validatedModels.length === 0) { throw new InstallerError({ @@ -49,27 +53,16 @@ export async function selectValidatedModel( return { model: selected }; } - const recommended = recommendedModels[0]; if (request.yes === true) { - if (recommended !== undefined) { - return { model: recommended }; - } - - if (validatedModels.length === 1) { - return { model: validatedModels[0]! }; - } - - throw new InstallerError({ - category: "model_registry", - code: "ambiguous_model_selection", - message: - "Multiple GonkaGate models are available; choose one with --model.", - }); + return { model: validatedModels[0]! }; } const selectedKey = await deps.prompts.select( "GonkaGate model", validatedModels.map((model) => ({ + ...(model.description === undefined + ? {} + : { description: model.description }), name: model.displayName, value: model.key, })), @@ -87,12 +80,10 @@ export async function selectValidatedModel( return { model: selected }; } -function getValidatedModelRecords( - registry: CuratedModelRegistry, -): CuratedModelRecord[] { +function getValidatedModelRecords(registry: ModelRegistry): ModelRecord[] { return Object.entries(registry) .map(([key, model]) => ({ - ...(model as CuratedModelDefinition), + ...(model as ModelDefinition), key, })) .filter((model) => model.validationStatus === "validated"); diff --git a/src/install/session.ts b/src/install/session.ts index f3d3ef7..b5a45fd 100644 --- a/src/install/session.ts +++ b/src/install/session.ts @@ -1,8 +1,8 @@ import { join } from "node:path"; import { - createCuratedModelIndex, + createModelIndex, formatMimoCodeModelRef, - type CuratedModelRegistry, + type ModelRegistry, } from "../constants/models.js"; import { GONKAGATE_PROVIDER_ID } from "../constants/gateway.js"; import type { InstallerBlocker, InstallerResult } from "./contracts.js"; @@ -38,7 +38,7 @@ export interface InstallSessionRequest { apiKeyStdin?: boolean; cwd?: string; modelKey?: string; - registry?: CuratedModelRegistry; + registry?: ModelRegistry; scope?: "user" | "project"; yes?: boolean; } @@ -126,8 +126,7 @@ export async function runInstallSession( }); } - const validatedModelKeys = - createCuratedModelIndex(registry).validatedModelKeys; + const validatedModelKeys = createModelIndex(registry).validatedModelKeys; const durableEffective = await verifyDurableEffectiveConfig(effectiveDeps, { modelKey: modelSelection.model.key, validatedModelKeys, diff --git a/test/cli.test.ts b/test/cli.test.ts index c44178c..af435e6 100644 --- a/test/cli.test.ts +++ b/test/cli.test.ts @@ -9,7 +9,7 @@ import { CURRENT_PROVIDER_PACKAGE, GONKAGATE_BASE_URL, } from "../src/constants/gateway.js"; -import type { CuratedModelRegistry } from "../src/constants/models.js"; +import type { ModelRegistry } from "../src/constants/models.js"; import { escapeRegExp, repoRoot } from "./contract-helpers.js"; import { createTestDeps } from "./install/test-deps.js"; @@ -33,11 +33,10 @@ const validatedRegistry = { adapterPackage: CURRENT_PROVIDER_PACKAGE, displayName: "Alpha", modelId: "provider/alpha", - recommended: true, transport: "chat_completions", validationStatus: "validated", }, -} as const satisfies CuratedModelRegistry; +} as const satisfies ModelRegistry; function queueCliSuccess( deps: ReturnType, diff --git a/test/docs-contract.test.ts b/test/docs-contract.test.ts index 0022577..1ef1129 100644 --- a/test/docs-contract.test.ts +++ b/test/docs-contract.test.ts @@ -86,17 +86,34 @@ test("model validation docs separate live catalog availability from workflow pro assertMatchesAll(modelValidation, [ /not the public picker allowlist/i, /workflow proof ledger/i, - /qwen\/qwen3-235b-a22b-instruct-2507-fp8/, /moonshotai\/kimi-k2\.6/, - /minimaxai\/minimax-m2\.7/, - /deepseek-ai\/deepseek-v4-flash-0731/, - /public GonkaGate models page/, - /400K context/, - /240K context/, - /180K context/, + /keeps no\s+copy of that catalog/s, /setCacheKey.*false/s, /mimo models gonkagate/, ]); + assert.doesNotMatch(modelValidation, /\d+K context/); +}); + +test("docs describe live model metadata and its pre-metadata fallbacks", () => { + const howItWorks = readText("docs/how-it-works.md"); + const readme = readText("README.md"); + const agents = readText("AGENTS.md"); + + assertMatchesAll(howItWorks, [ + /context_length/, + /no checked-in model catalog/, + /falls back to the model id/, + /writes no `limit` block/, + ]); + assertMatchesAll(readme, [ + /ships no model catalog and no default model id/, + /first model of the live `\/v1\/models`\s+response/s, + ]); + assertMatchesAll(agents, [ + /must never be checked in/, + /`data\[0\]` of the live response/, + /rather than a `0` limit/, + ]); }); test("PRD remains the product source of truth", () => { diff --git a/test/install/managed-provider-config.test.ts b/test/install/managed-provider-config.test.ts index f2d0f41..4465413 100644 --- a/test/install/managed-provider-config.test.ts +++ b/test/install/managed-provider-config.test.ts @@ -5,7 +5,7 @@ import { GONKAGATE_BASE_URL, MANAGED_SECRET_FILE_REF, } from "../../src/constants/gateway.js"; -import type { CuratedModelRegistry } from "../../src/constants/models.js"; +import type { ModelRegistry } from "../../src/constants/models.js"; import { createManagedProviderConfig, createManagedProviderConfigPatch, @@ -17,7 +17,6 @@ const validatedRegistry = { displayName: "Alpha", limits: { context: 10, output: 20 }, modelId: "provider/alpha", - recommended: true, runtimeCompatibility: { modelHeaders: { "x-test": "1" }, modelOptions: { temperature: 0 }, @@ -29,11 +28,10 @@ const validatedRegistry = { adapterPackage: CURRENT_PROVIDER_PACKAGE, displayName: "Beta", modelId: "provider/beta", - recommended: false, transport: "chat_completions", validationStatus: "validated", }, -} as const satisfies CuratedModelRegistry; +} as const satisfies ModelRegistry; test("managed provider config writes only validated models with canonical provider options", () => { const empty = createManagedProviderConfig({}); @@ -47,7 +45,32 @@ test("managed provider config writes only validated models with canonical provid assert.deepEqual(Object.keys(config.models), ["alpha", "beta"]); assert.deepEqual(config.models.alpha?.limit, { context: 10, output: 20 }); assert.deepEqual(config.models.alpha?.headers, { "x-test": "1" }); - assert.deepEqual(config.models.beta?.limit, { context: 0, output: 0 }); + assert.equal(config.models.alpha?.name, "Alpha"); +}); + +test("generated model entries carry the live display name and context window", () => { + const config = createManagedProviderConfig({ + "deepseek-ai/deepseek-v4-flash-0731": { + adapterPackage: CURRENT_PROVIDER_PACKAGE, + displayName: "DeepSeek V4 Flash 0731", + limits: { context: 400_000 }, + modelId: "deepseek-ai/deepseek-v4-flash-0731", + transport: "chat_completions", + validationStatus: "validated", + }, + }); + + assert.deepEqual(config.models["deepseek-ai/deepseek-v4-flash-0731"], { + limit: { context: 400_000, output: 0 }, + name: "DeepSeek V4 Flash 0731", + }); +}); + +test("no limit block is written when the gateway publishes no context window", () => { + const config = createManagedProviderConfig(validatedRegistry); + + assert.equal("limit" in (config.models.beta ?? {}), false); + assert.deepEqual(config.models.beta, { name: "Beta" }); }); test("candidate-only registry is not exposed in generated runtime provider catalog", () => { @@ -56,11 +79,10 @@ test("candidate-only registry is not exposed in generated runtime provider catal adapterPackage: CURRENT_PROVIDER_PACKAGE, displayName: "Candidate", modelId: "provider/candidate", - recommended: false, transport: "chat_completions", validationStatus: "candidate", }, - } as const satisfies CuratedModelRegistry; + } as const satisfies ModelRegistry; assert.deepEqual(createManagedProviderConfig(candidateOnly).models, {}); }); @@ -71,14 +93,13 @@ test("provider config rejects compatibility metadata that overrides canonical se adapterPackage: CURRENT_PROVIDER_PACKAGE, displayName: "Alpha", modelId: "provider/alpha", - recommended: true, runtimeCompatibility: { providerOptions: { baseURL: "https://evil.test" }, }, transport: "chat_completions", validationStatus: "validated", }, - } as const satisfies CuratedModelRegistry; + } as const satisfies ModelRegistry; assert.throws(() => createManagedProviderConfig(invalid), /cannot override/); assert.deepEqual(createManagedProviderConfigPatch(validatedRegistry).path, [ diff --git a/test/install/model-catalog.test.ts b/test/install/model-catalog.test.ts index fb372d6..9893859 100644 --- a/test/install/model-catalog.test.ts +++ b/test/install/model-catalog.test.ts @@ -9,11 +9,22 @@ import { } from "../../src/install/model-catalog.js"; import { createTestDeps } from "./test-deps.js"; -const catalogBody = { +const legacyCatalogBody = { data: [ - { id: "moonshotai/kimi-k2.6", object: "model", owned_by: "gonka" }, - { id: "minimaxai/minimax-m2.7", object: "model", owned_by: "gonka" }, { + created: 0, + id: "moonshotai/kimi-k2.6", + object: "model", + owned_by: "gonka", + }, + { + created: 0, + id: "minimaxai/minimax-m2.7", + object: "model", + owned_by: "gonka", + }, + { + created: 0, id: "qwen/qwen3-235b-a22b-instruct-2507-fp8", object: "model", owned_by: "gonka", @@ -24,7 +35,7 @@ const catalogBody = { test("GonkaGate model catalog fetch builds a runtime registry from every returned model id", async () => { const deps = createTestDeps(); - deps.queueHttpResponse({ body: catalogBody, status: 200 }); + deps.queueHttpResponse({ body: legacyCatalogBody, status: 200 }); try { const registry = await fetchGonkaGateModelCatalog(deps, "gp-secret-value"); @@ -43,8 +54,6 @@ test("GonkaGate model catalog fetch builds a runtime registry from every returne registry["moonshotai/kimi-k2.6"]?.adapterPackage, CURRENT_PROVIDER_PACKAGE, ); - assert.equal(registry["moonshotai/kimi-k2.6"]?.recommended, true); - assert.equal(registry["minimaxai/minimax-m2.7"]?.recommended, false); assert.equal( registry["qwen/qwen3-235b-a22b-instruct-2507-fp8"]?.validationStatus, "validated", @@ -54,6 +63,90 @@ test("GonkaGate model catalog fetch builds a runtime registry from every returne } }); +test("catalog metadata is read live when the gateway publishes it", () => { + const registry = parseGonkaGateModelCatalog({ + data: [ + { + context_length: 400_000, + created: 1_753_920_000, + description: "Fast general-purpose model.", + id: "deepseek-ai/deepseek-v4-flash-0731", + name: "DeepSeek V4 Flash 0731", + object: "model", + owned_by: "gonka", + }, + { + contextLength: 240_000, + id: "moonshotai/kimi-k2.6", + name: "Kimi K2.6", + object: "model", + }, + ], + object: "list", + }); + + assert.deepEqual(Object.keys(registry), [ + "deepseek-ai/deepseek-v4-flash-0731", + "moonshotai/kimi-k2.6", + ]); + assert.equal( + registry["deepseek-ai/deepseek-v4-flash-0731"]?.displayName, + "DeepSeek V4 Flash 0731", + ); + assert.equal( + registry["deepseek-ai/deepseek-v4-flash-0731"]?.description, + "Fast general-purpose model.", + ); + assert.deepEqual(registry["deepseek-ai/deepseek-v4-flash-0731"]?.limits, { + context: 400_000, + }); + assert.equal(registry["moonshotai/kimi-k2.6"]?.displayName, "Kimi K2.6"); + assert.deepEqual(registry["moonshotai/kimi-k2.6"]?.limits, { + context: 240_000, + }); +}); + +test("absent, null, and unusable catalog metadata falls back instead of failing", () => { + const registry = parseGonkaGateModelCatalog({ + data: [ + { created: 0, id: "provider/legacy", object: "model", owned_by: "gonka" }, + { + context_length: null, + description: null, + id: "provider/nulls", + name: null, + object: "model", + }, + { + context_length: 0, + description: " ", + id: "provider/unusable", + name: " ", + object: "model", + }, + { + context_length: "400000", + id: "provider/wrong-types", + name: 42, + object: "model", + }, + ], + object: "list", + }); + + for (const key of [ + "provider/legacy", + "provider/nulls", + "provider/unusable", + "provider/wrong-types", + ]) { + assert.equal(registry[key]?.displayName, key); + assert.equal(registry[key]?.description, undefined); + assert.equal(registry[key]?.limits, undefined); + assert.equal(registry[key]?.validationStatus, "validated"); + } +}); + test("GonkaGate model catalog maps auth and malformed catalog failures to installer errors", async () => { const auth = createTestDeps(); auth.queueHttpResponse({ diff --git a/test/install/rerun.test.ts b/test/install/rerun.test.ts index 70f6239..5e3d0d4 100644 --- a/test/install/rerun.test.ts +++ b/test/install/rerun.test.ts @@ -3,7 +3,7 @@ import { readdirSync } from "node:fs"; import { join } from "node:path"; import test from "node:test"; import { CURRENT_PROVIDER_PACKAGE } from "../../src/constants/gateway.js"; -import type { CuratedModelRegistry } from "../../src/constants/models.js"; +import type { ModelRegistry } from "../../src/constants/models.js"; import { runInstallSession } from "../../src/install/session.js"; import { createTestDeps } from "./test-deps.js"; @@ -12,7 +12,6 @@ const registry = { adapterPackage: CURRENT_PROVIDER_PACKAGE, displayName: "Alpha", modelId: "provider/alpha", - recommended: true, transport: "chat_completions", validationStatus: "validated", }, @@ -20,11 +19,10 @@ const registry = { adapterPackage: CURRENT_PROVIDER_PACKAGE, displayName: "Beta", modelId: "provider/beta", - recommended: false, transport: "chat_completions", validationStatus: "validated", }, -} as const satisfies CuratedModelRegistry; +} as const satisfies ModelRegistry; function resolved(model: "alpha" | "beta") { return JSON.stringify({ diff --git a/test/install/scope.test.ts b/test/install/scope.test.ts index 12bfea9..ea600d1 100644 --- a/test/install/scope.test.ts +++ b/test/install/scope.test.ts @@ -1,7 +1,7 @@ import assert from "node:assert/strict"; import test from "node:test"; import { CURRENT_PROVIDER_PACKAGE } from "../../src/constants/gateway.js"; -import type { CuratedModelRegistry } from "../../src/constants/models.js"; +import type { ModelRegistry } from "../../src/constants/models.js"; import { getConfigValue } from "../../src/install/config-value.js"; import { parseJsoncDocument } from "../../src/install/jsonc.js"; import { @@ -14,11 +14,10 @@ const registry = { adapterPackage: CURRENT_PROVIDER_PACKAGE, displayName: "Alpha", modelId: "provider/alpha", - recommended: true, transport: "chat_completions", validationStatus: "validated", }, -} as const satisfies CuratedModelRegistry; +} as const satisfies ModelRegistry; test("user scope writes provider and activation to global config only", () => { const plan = createScopeWritePlan({ @@ -70,11 +69,10 @@ test("candidate-only registry writes no public provider model catalog entries", adapterPackage: CURRENT_PROVIDER_PACKAGE, displayName: "Candidate", modelId: "provider/candidate", - recommended: false, transport: "chat_completions", validationStatus: "candidate", }, - } as const satisfies CuratedModelRegistry; + } as const satisfies ModelRegistry; const plan = createScopeWritePlan({ modelKey: "candidate", registry: candidateOnly, diff --git a/test/install/selection.test.ts b/test/install/selection.test.ts index ab71e01..08c8f96 100644 --- a/test/install/selection.test.ts +++ b/test/install/selection.test.ts @@ -1,7 +1,7 @@ import assert from "node:assert/strict"; import test from "node:test"; import { CURRENT_PROVIDER_PACKAGE } from "../../src/constants/gateway.js"; -import type { CuratedModelRegistry } from "../../src/constants/models.js"; +import type { ModelRegistry } from "../../src/constants/models.js"; import { InstallerError } from "../../src/install/errors.js"; import { selectScope, @@ -14,11 +14,10 @@ const oneValidated = { adapterPackage: CURRENT_PROVIDER_PACKAGE, displayName: "Alpha", modelId: "provider/alpha", - recommended: true, transport: "chat_completions", validationStatus: "validated", }, -} as const satisfies CuratedModelRegistry; +} as const satisfies ModelRegistry; const twoValidated = { alpha: oneValidated.alpha, @@ -26,11 +25,10 @@ const twoValidated = { adapterPackage: CURRENT_PROVIDER_PACKAGE, displayName: "Beta", modelId: "provider/beta", - recommended: false, transport: "chat_completions", validationStatus: "validated", }, -} as const satisfies CuratedModelRegistry; +} as const satisfies ModelRegistry; test("validated-only selection blocks candidate-only and unsupported model keys", async () => { const deps = createTestDeps(); @@ -41,7 +39,6 @@ test("validated-only selection blocks candidate-only and unsupported model keys" adapterPackage: CURRENT_PROVIDER_PACKAGE, displayName: "Candidate", modelId: "provider/candidate", - recommended: false, transport: "chat_completions", validationStatus: "candidate", }, @@ -64,7 +61,7 @@ test("validated-only selection blocks candidate-only and unsupported model keys" deps.cleanup(); }); -test("validated-only selection supports recommended, single, prompt, and ambiguity behavior", async () => { +test("non-interactive selection defaults to the first live catalog entry", async () => { const deps = createTestDeps(); assert.equal( (await selectValidatedModel({ yes: true }, deps, oneValidated)).model.key, @@ -76,23 +73,36 @@ test("validated-only selection supports recommended, single, prompt, and ambigui "alpha", ); assert.equal( - (await selectValidatedModel({}, deps, twoValidated)).model.key, + (await selectValidatedModel({ yes: true }, deps, twoValidated)).model.key, "alpha", ); - const ambiguousDeps = createTestDeps(); - const noRecommended = { - alpha: { ...oneValidated.alpha, recommended: false }, - beta: { ...twoValidated.beta, recommended: false }, - } as const satisfies CuratedModelRegistry; - await assert.rejects( - () => selectValidatedModel({ yes: true }, ambiguousDeps, noRecommended), - (error) => { - assert.equal((error as InstallerError).code, "ambiguous_model_selection"); - return true; - }, + const reversed = { + beta: twoValidated.beta, + alpha: twoValidated.alpha, + } as const satisfies ModelRegistry; + assert.equal( + (await selectValidatedModel({ yes: true }, deps, reversed)).model.key, + "beta", ); assert.equal(await selectScope(undefined, deps, true), "user"); deps.cleanup(); - ambiguousDeps.cleanup(); +}); + +test("interactive picker offers live catalog order, display names, and descriptions", async () => { + const deps = createTestDeps(); + const described = { + alpha: { ...twoValidated.alpha, displayName: "Alpha One" }, + beta: { ...twoValidated.beta, description: "Fast beta model" }, + } as const satisfies ModelRegistry; + + assert.equal( + (await selectValidatedModel({}, deps, described)).model.key, + "alpha", + ); + assert.deepEqual(deps.selectPromptLog[0]?.choices, [ + { name: "Alpha One", value: "alpha" }, + { description: "Fast beta model", name: "Beta", value: "beta" }, + ]); + deps.cleanup(); }); diff --git a/test/install/session.test.ts b/test/install/session.test.ts index d13dcfd..e5089b2 100644 --- a/test/install/session.test.ts +++ b/test/install/session.test.ts @@ -2,7 +2,7 @@ import assert from "node:assert/strict"; import { join } from "node:path"; import test from "node:test"; import { CURRENT_PROVIDER_PACKAGE } from "../../src/constants/gateway.js"; -import type { CuratedModelRegistry } from "../../src/constants/models.js"; +import type { ModelRegistry } from "../../src/constants/models.js"; import { runInstallSession } from "../../src/install/session.js"; import { createTestDeps } from "./test-deps.js"; @@ -11,11 +11,10 @@ const registry = { adapterPackage: CURRENT_PROVIDER_PACKAGE, displayName: "Alpha", modelId: "provider/alpha", - recommended: true, transport: "chat_completions", validationStatus: "validated", }, -} as const satisfies CuratedModelRegistry; +} as const satisfies ModelRegistry; function matchingConfig() { return JSON.stringify({ @@ -57,6 +56,13 @@ function matchingLiveConfig() { }); } +function getWrittenModels(contents: string): Record { + const parsed = JSON.parse(contents) as { + provider: { gonkagate: { models: Record } }; + }; + return parsed.provider.gonkagate.models; +} + function prepareDeps() { const deps = createTestDeps(); const home = join(deps.root, "home"); @@ -150,13 +156,79 @@ test("install session fetches the live GonkaGate catalog and writes every return const globalConfig = await deps.fs.readText( join(configDir, "mimocode.jsonc"), ); + const written = getWrittenModels(globalConfig); assert.equal(result.status, "success"); assert.equal(result.ok, true); assert.equal(result.model, "moonshotai/kimi-k2.6"); - assert.match(globalConfig, /moonshotai\/kimi-k2\.6/); - assert.match(globalConfig, /minimaxai\/minimax-m2\.7/); - assert.match(globalConfig, /qwen\/qwen3-235b-a22b-instruct-2507-fp8/); + assert.deepEqual(Object.keys(written), [ + "moonshotai/kimi-k2.6", + "minimaxai/minimax-m2.7", + "qwen/qwen3-235b-a22b-instruct-2507-fp8", + ]); + assert.deepEqual(written["moonshotai/kimi-k2.6"], { + name: "moonshotai/kimi-k2.6", + }); + assert.doesNotMatch(globalConfig, /"context"/); + } finally { + deps.cleanup(); + } +}); + +test("install session carries live model name and context window into MiMoCode config", async () => { + const { deps, home } = prepareDeps(); + const configDir = join(home, ".config", "mimocode"); + queueSuccessfulLiveCommands(deps, configDir); + deps.queueHttpResponse({ + body: { + data: [ + { + context_length: 240_000, + created: 1_753_920_000, + description: "Long-context coding model.", + id: "moonshotai/kimi-k2.6", + name: "Kimi K2.6", + object: "model", + owned_by: "gonka", + }, + { + context_length: null, + id: "minimaxai/minimax-m2.7", + object: "model", + owned_by: "gonka", + }, + { + context_length: 180_000, + id: "qwen/qwen3-235b-a22b-instruct-2507-fp8", + name: "Qwen3 235B", + object: "model", + owned_by: "gonka", + }, + ], + object: "list", + }, + status: 200, + }); + + try { + const result = await runInstallSession({ scope: "user", yes: true }, deps); + const written = getWrittenModels( + await deps.fs.readText(join(configDir, "mimocode.jsonc")), + ); + + assert.equal(result.status, "success"); + assert.equal(result.model, "moonshotai/kimi-k2.6"); + assert.deepEqual(written["moonshotai/kimi-k2.6"], { + limit: { context: 240_000, output: 0 }, + name: "Kimi K2.6", + }); + assert.deepEqual(written["minimaxai/minimax-m2.7"], { + name: "minimaxai/minimax-m2.7", + }); + assert.deepEqual(written["qwen/qwen3-235b-a22b-instruct-2507-fp8"], { + limit: { context: 180_000, output: 0 }, + name: "Qwen3 235B", + }); } finally { deps.cleanup(); } diff --git a/test/install/test-deps.ts b/test/install/test-deps.ts index 64f110e..d726bd5 100644 --- a/test/install/test-deps.ts +++ b/test/install/test-deps.ts @@ -22,6 +22,11 @@ export interface RecordedHttpRequest { url: string; } +export interface RecordedSelectPrompt { + choices: readonly { description?: string; name: string; value: string }[]; + message: string; +} + export interface TestDeps extends InstallerDeps { cleanup(): void; commandLog: RecordedCommand[]; @@ -31,6 +36,7 @@ export interface TestDeps extends InstallerDeps { queueHttpResponse(result: HttpJsonResponse): void; queuePrompt(value: string): void; root: string; + selectPromptLog: RecordedSelectPrompt[]; setCwd(path: string): void; setEnv(env: NodeJS.ProcessEnv): void; setStdin(contents: string): void; @@ -47,6 +53,7 @@ export function createTestDeps(): TestDeps { const httpLog: RecordedHttpRequest[] = []; const passwordPromptLog: PasswordPromptOptions[] = []; const promptValues: string[] = []; + const selectPromptLog: RecordedSelectPrompt[] = []; const deps: TestDeps = { cleanup() { @@ -90,7 +97,8 @@ export function createTestDeps(): TestDeps { passwordPromptLog.push(options ?? {}); return promptValues.shift() ?? ""; }, - async select(_message, choices) { + async select(message, choices) { + selectPromptLog.push({ choices: [...choices], message }); return choices[0]?.value ?? ""; }, }, @@ -105,6 +113,7 @@ export function createTestDeps(): TestDeps { promptValues.push(value); }, root, + selectPromptLog, setCwd(path) { cwd = path; }, diff --git a/test/install/verify-layers.test.ts b/test/install/verify-layers.test.ts index 7ca39b7..28365da 100644 --- a/test/install/verify-layers.test.ts +++ b/test/install/verify-layers.test.ts @@ -16,7 +16,6 @@ test("generated project-scope config stays commit-safe", () => { adapterPackage: CURRENT_PROVIDER_PACKAGE, displayName: "Alpha", modelId: "provider/alpha", - recommended: true, transport: "chat_completions", validationStatus: "validated", }, diff --git a/test/package-contract.test.ts b/test/package-contract.test.ts index 593a79c..2c629c5 100644 --- a/test/package-contract.test.ts +++ b/test/package-contract.test.ts @@ -12,14 +12,7 @@ import { MANAGED_SECRET_FILE_REF, TARGET_CLI, } from "../src/constants/gateway.js"; -import { - CURATED_MODEL_REGISTRY, - SUPPORTED_MODEL_KEYS, - type CuratedModelRegistry, - formatMimoCodeModelRef, - getRecommendedValidatedModel, - getValidatedModels, -} from "../src/constants/models.js"; +import { formatMimoCodeModelRef } from "../src/constants/models.js"; import { MODEL_VALIDATION_RECORDS } from "../src/constants/model-validation.js"; import { readText } from "./contract-helpers.js"; @@ -118,62 +111,21 @@ test("constants pin the planned GonkaGate MiMoCode provider contract", () => { assert.equal(CONTRACT_METADATA.mimoCode.packageName, "@mimo-ai/cli"); }); -test("curated model registry is present but not falsely validated", () => { - assert.deepEqual(SUPPORTED_MODEL_KEYS, [ - "deepseek-ai/deepseek-v4-flash-0731", - "moonshotai/kimi-k2.6", - "minimaxai/minimax-m2.7", - "qwen/qwen3-235b-a22b-instruct-2507-fp8", - ]); - assert.equal(getValidatedModels().length, 1); - assert.equal(getValidatedModels()[0]?.key, "moonshotai/kimi-k2.6"); - assert.equal(getRecommendedValidatedModel()?.key, "moonshotai/kimi-k2.6"); - assert.equal(CONTRACT_METADATA.curatedRegistryPublished, true); +test("model metadata constants keep no checked-in catalog", () => { + const modelConstants = readText("src/constants/models.ts"); - for (const [key, model] of Object.entries(CURATED_MODEL_REGISTRY)) { - assert.equal(model.adapterPackage, "@ai-sdk/openai-compatible"); - assert.equal(model.transport, "chat_completions"); - assert.equal( - model.validationStatus, - key === "moonshotai/kimi-k2.6" ? "validated" : "candidate", - ); - assert.equal(model.recommended, key === "moonshotai/kimi-k2.6"); - } - - assert.equal( - CURATED_MODEL_REGISTRY["deepseek-ai/deepseek-v4-flash-0731"].modelId, - "deepseek-ai/deepseek-v4-flash-0731", - ); - assert.equal( - CURATED_MODEL_REGISTRY["deepseek-ai/deepseek-v4-flash-0731"].limits - ?.context, - 400_000, - ); - assert.equal( - CURATED_MODEL_REGISTRY["moonshotai/kimi-k2.6"].modelId, - "moonshotai/kimi-k2.6", - ); + assert.doesNotMatch(modelConstants, /deepseek|kimi|minimax|qwen/i); + assert.doesNotMatch(modelConstants, /context:\s*\d/); + assert.doesNotMatch(modelConstants, /recommended/i); assert.equal( - CURATED_MODEL_REGISTRY["moonshotai/kimi-k2.6"].limits?.context, - 262_000, - ); - assert.equal( - CURATED_MODEL_REGISTRY["minimaxai/minimax-m2.7"].modelId, - "minimaxai/minimax-m2.7", - ); - assert.equal( - CURATED_MODEL_REGISTRY["minimaxai/minimax-m2.7"].limits?.context, - 205_000, - ); - assert.equal( - CURATED_MODEL_REGISTRY["qwen/qwen3-235b-a22b-instruct-2507-fp8"].modelId, - "qwen/qwen3-235b-a22b-instruct-2507-fp8", - ); - assert.equal( - CURATED_MODEL_REGISTRY["qwen/qwen3-235b-a22b-instruct-2507-fp8"].limits - ?.context, - 262_000, + Object.keys(CONTRACT_METADATA).includes("curatedRegistry"), + false, ); + assert.match(CONTRACT_METADATA.publicState, /\/v1\/models/); + + const catalog = readText("src/install/model-catalog.ts"); + assert.match(catalog, /context_length/); + assert.match(catalog, /contextLength/); assert.equal( formatMimoCodeModelRef("moonshotai/kimi-k2.6"), @@ -181,16 +133,14 @@ test("curated model registry is present but not falsely validated", () => { ); }); -test("validated MiMoCode model registry entries require validation records", () => { - for (const [key, model] of Object.entries( - CURATED_MODEL_REGISTRY as CuratedModelRegistry, - )) { - if (model.validationStatus === "validated") { - assert.ok( - key in MODEL_VALIDATION_RECORDS, - `${key} is validated without a validation record`, - ); - } +test("the MiMoCode workflow proof ledger stays self-consistent", () => { + const records = Object.entries(MODEL_VALIDATION_RECORDS); + assert.ok(records.length > 0); + + for (const [key, record] of records) { + assert.equal(record.modelKey, key); + assert.equal(record.providerPackage, "@ai-sdk/openai-compatible"); + assert.equal(record.transport, "chat_completions"); } const kimi = MODEL_VALIDATION_RECORDS["moonshotai/kimi-k2.6"];