diff --git a/AGENTS.md b/AGENTS.md index 72e4c32..87b71a5 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -32,13 +32,11 @@ Current honest state: sandbox XDG config tree so project-scope verification matches the local resolver on `@kilocode/cli >=7.2.0` installs, and that temporary sandbox now stages outside the repository with cleanup after verification -- the stock public build now ships `moonshotai/Kimi-K2.6` as the recommended - validated curated default, with installer-managed +- the installer now fetches `GET https://api.gonkagate.com/v1/models` after + safe API-key intake and uses that live response as the model source of truth +- the managed GonkaGate provider config writes every fetched chat-completions + model into Kilo's OpenCode-style `/models` picker with installer-managed `limit.output = 8192` for Kilo `7.2.0` compatibility -- the managed GonkaGate provider config now writes the validated - chat-completions catalog, currently Kimi K2.6, Qwen3 235B A22B Instruct - 2507 FP8, and MiniMax M2.7, so Kilo's OpenCode-style `/models` picker can - switch between validated GonkaGate models - Kilo detection now accepts `@kilocode/cli >=7.2.0` without pre-blocking future Kilo releases, while the audited compatibility baseline remains `@kilocode/cli@7.2.0` @@ -59,12 +57,12 @@ The intended happy path is: 1. user runs `npx @gonkagate/kilo-setup` 2. installer validates local `kilo` or fallback `kilocode` -3. installer offers curated validated Kilo model choices -4. installer auto-selects `project` inside a git repository or `user` +3. installer collects a GonkaGate key through a hidden prompt, + `GONKAGATE_API_KEY`, or `--api-key-stdin` +4. installer fetches live Kilo model choices from GonkaGate `/v1/models` +5. installer auto-selects `project` inside a git repository or `user` otherwise, and only asks on interactive reruns when the previous installer-managed scope differs from the new recommendation -5. installer collects a GonkaGate key through a hidden prompt, - `GONKAGATE_API_KEY`, or `--api-key-stdin` 6. installer writes the minimum safe Kilo config layers 7. for `project` installs, installer reports Kilo global UI-model cache risk and can clear the current cached model on request @@ -134,10 +132,8 @@ These are implementation facts today: - `src/install/` contains shipped Kilo detection, path resolution, secret intake, managed config writes, rollback, effective-config verification, and orchestration -- `src/constants/models.ts` now exposes Kimi K2.6 as the recommended validated - production default plus the validated Qwen3 235B A22B Instruct 2507 FP8 - and MiniMax M2.7 catalog entries, each with installer-managed - `limit.output = 8192` in the written Kilo provider config +- `src/install/model-catalog.ts` fetches and parses GonkaGate `/v1/models` as + the installer model source of truth - `docs/specs/kilo-setup-prd/spec.md` is the copied Kilo setup PRD - `docs/release-readiness.md` records the current production-readiness audit - `.github/workflows/release-please.yml` and `.github/workflows/publish.yml` diff --git a/CHANGELOG.md b/CHANGELOG.md index d4e76c0..3ef2d73 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,9 @@ ## Unreleased +- Fetched GonkaGate model availability from authenticated `GET /v1/models` + after safe API-key intake, using the live response for the model picker, + config writes, and `--model` validation. - Updated managed GonkaGate `limit.context` values to match the current deployed windows: `240000` for Kimi/Qwen and `180000` for MiniMax. - Wrote the validated GonkaGate chat-completions catalog into the managed diff --git a/README.md b/README.md index 06edd4e..e6c1324 100644 --- a/README.md +++ b/README.md @@ -72,13 +72,10 @@ Current public baseline: - package: `@gonkagate/kilo-setup` - primary command after setup: `kilo` - current transport target: `chat/completions` -- curated default: - `moonshotai/Kimi-K2.6` -- managed validated catalog for Kilo's OpenCode-style `/models` picker: - `moonshotai/Kimi-K2.6`, - `qwen/qwen3-235b-a22b-instruct-2507-fp8`, and - `minimaxai/minimax-m2.7` -- installer-managed `limit.output = 8192` for Kilo compatibility +- model source of truth: `GET https://api.gonkagate.com/v1/models` after safe + API-key intake +- Kilo's OpenCode-style `/models` picker receives every fetched GonkaGate model +- installer-managed `limit.output = 8192` for each fetched model entry - no native Windows production claim yet ## Shortest Start Path @@ -95,15 +92,16 @@ The installer will: 1. detect `kilo`, or fall back to `kilocode` 2. verify local Kilo is at least `@kilocode/cli >=7.2.0` -3. show the curated model choice -4. write the validated GonkaGate provider model catalog for Kilo's `/models` +3. collect your API key through a hidden prompt +4. fetch the live GonkaGate model catalog from `/v1/models` +5. show the live model choices +6. write the fetched GonkaGate provider model catalog for Kilo's `/models` picker -5. choose the recommended scope automatically: +7. choose the recommended scope automatically: - inside a git repository: `project` - outside a repository: `user` -6. collect your API key through a hidden prompt -7. write the managed config and verify the result -8. return you to normal `kilo` usage +8. write the managed config and verify the result +9. return you to normal `kilo` usage On interactive reruns, the installer asks about scope only if the last installer-managed scope differs from the new recommendation. @@ -205,18 +203,14 @@ This repository intentionally stays narrow today: - minimum accepted Kilo floor: `@kilocode/cli >=7.2.0` - current transport target: `chat/completions` -- current curated default: - `moonshotai/Kimi-K2.6` -- validated GonkaGate models exposed to Kilo's `/models` picker: - `moonshotai/Kimi-K2.6`, - `qwen/qwen3-235b-a22b-instruct-2507-fp8`, and - `minimaxai/minimax-m2.7` +- model availability comes from authenticated `GET /v1/models` +- if no explicit model is selected, setup uses the API-provided default when + present, otherwise the first returned model - real-path Kilo verification is not the production default - native Windows production support is not claimed yet - future Kilo releases are not pre-blocked by version, but observed compatibility breaks still need fixes -- unvalidated extra models and new flows are not implied just because this - package exists +- new transport flows are not implied just because this package exists The shipped runtime treats effective Kilo config as the real success gate. It uses the local resolver as the durable verifier and keeps the XDG-isolated diff --git a/docs/README.md b/docs/README.md index 07e8deb..df78300 100644 --- a/docs/README.md +++ b/docs/README.md @@ -22,9 +22,7 @@ Current contract documents: - [`specs/kilo-setup-prd/spec.md`](./specs/kilo-setup-prd/spec.md): copied Kilo setup PRD from the planning repository -This repository ships the Kilo installer runtime with a validated curated -GonkaGate chat-completions catalog, Kimi K2.6 as the recommended default, -MiniMax M2.7 and Qwen3 235B A22B Instruct 2507 FP8 as additional validated -catalog entries, installer-managed `limit.output = 8192` for Kilo -compatibility, and a minimum accepted Kilo floor of `@kilocode/cli >=7.2.0` -without a preset upper version bound. +This repository ships the Kilo installer runtime with authenticated GonkaGate +`GET /v1/models` discovery as the model source of truth, installer-managed +`limit.output = 8192` for each fetched Kilo model entry, and a minimum accepted +Kilo floor of `@kilocode/cli >=7.2.0` without a preset upper version bound. diff --git a/docs/gonkagate-x-kilo.md b/docs/gonkagate-x-kilo.md index ad9db1d..6738119 100644 --- a/docs/gonkagate-x-kilo.md +++ b/docs/gonkagate-x-kilo.md @@ -129,16 +129,13 @@ Right now the public default is deliberately small: - base URL: `https://api.gonkagate.com/v1` - transport: `chat/completions` - minimum Kilo floor: `@kilocode/cli >=7.2.0` -- recommended validated model: `moonshotai/Kimi-K2.6` -- validated catalog exposed to Kilo's OpenCode-style `/models` picker: - `moonshotai/Kimi-K2.6`, - `qwen/qwen3-235b-a22b-instruct-2507-fp8`, and - `minimaxai/minimax-m2.7` -- managed limits: `limit.output = 8192` for all validated entries; - `limit.context = 240000` for Kimi/Qwen and `180000` for MiniMax - -We are treating model support as a curated list, not as a vague "it probably -works" promise. +- model source of truth: authenticated `GET /v1/models` +- catalog exposed to Kilo's OpenCode-style `/models` picker: every fetched + GonkaGate model id +- managed limit: `limit.output = 8192` for each fetched model entry + +We are treating model availability as GonkaGate API truth, not as a checked-in +package catalog. ## What we are not claiming diff --git a/docs/how-it-works.md b/docs/how-it-works.md index c3d0724..bf0ec2e 100644 --- a/docs/how-it-works.md +++ b/docs/how-it-works.md @@ -29,48 +29,46 @@ Current public limit: - the published contract keeps a minimum Kilo floor of `@kilocode/cli >=7.2.0`, `chat/completions`, and non-Windows production claims -- the curated default is - `moonshotai/Kimi-K2.6` with `limit.context = 240000` and - `limit.output = 8192` -- the written provider config includes the validated chat-completions model - catalog, currently `moonshotai/Kimi-K2.6`, - `qwen/qwen3-235b-a22b-instruct-2507-fp8`, and - `minimaxai/minimax-m2.7`, so Kilo's OpenCode-style `/models` picker can - switch between validated GonkaGate models -- broader model claims still require additional proof before models should be - added to the validated catalog +- model availability comes from authenticated `GET /v1/models` after safe + API-key intake +- the written provider config includes every fetched GonkaGate + chat-completions model so Kilo's OpenCode-style `/models` picker can switch + between the live GonkaGate options +- each fetched model entry gets installer-managed `limit.output = 8192` for + Kilo `7.2.0` compatibility ## Install Flow 1. Check that `kilo` is available, or fall back to `kilocode`. 2. Verify the minimum accepted Kilo floor: `@kilocode/cli >=7.2.0`. -3. Resolve the curated model choice and scope. -4. Use the recommended scope automatically in the default interactive flow: - - `project` inside a git repository - - `user` outside a repository -5. On interactive reruns, ask about scope only when the previous - installer-managed scope differs from the new recommendation. -6. Accept a GonkaGate API key through: +3. Accept a GonkaGate API key through: - a hidden interactive prompt - `GONKAGATE_API_KEY` - `--api-key-stdin` -7. Save the secret only under `~/.gonkagate/kilo/api-key`. -8. Write or update the user-level provider definition, including the validated +4. Fetch the live GonkaGate model catalog from `GET /v1/models`. +5. Resolve the live model choice and scope. +6. Use the recommended scope automatically in the default interactive flow: + - `project` inside a git repository + - `user` outside a repository +7. On interactive reruns, ask about scope only when the previous + installer-managed scope differs from the new recommendation. +8. Save the secret only under `~/.gonkagate/kilo/api-key`. +9. Write or update the user-level provider definition, including the fetched GonkaGate model catalog for Kilo's `/models` picker. -9. When `project` scope is chosen, write only activation settings into - `.kilo/kilo.jsonc`. -10. On rerun, remove only installer-owned stale GonkaGate activation from the +10. When `project` scope is chosen, write only activation settings into + `.kilo/kilo.jsonc`. +11. On rerun, remove only installer-owned stale GonkaGate activation from the old location and preserve unrelated Kilo config. -11. Verify the durable intended Kilo outcome with the local resolver and use +12. Verify the durable intended Kilo outcome with the local resolver and use the XDG-isolated oracle as a compatibility check. -12. If `KILO_CONFIG_CONTENT` is active, or the installer is running inside an +13. If `KILO_CONFIG_CONTENT` is active, or the installer is running inside an active `kilo` terminal session with runtime config overrides, verify the current session separately. -13. For `project` installs, surface Kilo global UI-model cache notices and +14. For `project` installs, surface Kilo global UI-model cache notices and optionally clear the current cached model when the user requests it. -14. Report redacted blockers or mismatches instead of printing raw resolved +15. Report redacted blockers or mismatches instead of printing raw resolved config. -15. Finish by sending the user back to plain `kilo`. +16. Finish by sending the user back to plain `kilo`. ## Why User-Level Provider Ownership diff --git a/docs/release-readiness.md b/docs/release-readiness.md index 21db173..dcce7f1 100644 --- a/docs/release-readiness.md +++ b/docs/release-readiness.md @@ -17,15 +17,11 @@ docs, and tests for these facts: - Kilo command contract: `kilo` first, `kilocode` as fallback - minimum Kilo compatibility floor: `@kilocode/cli >=7.2.0` - current GonkaGate transport claim: `chat/completions` -- curated public default: - `moonshotai/Kimi-K2.6` -- validated provider catalog exposed to Kilo's OpenCode-style `/models` picker: - `moonshotai/Kimi-K2.6`, - `qwen/qwen3-235b-a22b-instruct-2507-fp8`, and - `minimaxai/minimax-m2.7` -- curated model limits for the validated catalog: - `limit.output = 8192` for all validated entries; `limit.context = 240000` - for Kimi/Qwen and `180000` for MiniMax +- model availability source: authenticated `GET /v1/models` +- provider catalog exposed to Kilo's OpenCode-style `/models` picker: every + fetched GonkaGate model id +- model limit written for Kilo compatibility: `limit.output = 8192` for each + fetched model entry - managed secret path: `~/.gonkagate/kilo/api-key` - project scope stays secret-free and still depends on a compatible user-level `provider.gonkagate` definition on each machine @@ -34,21 +30,16 @@ docs, and tests for these facts: ## External Evidence Captured -The current model-limit contract is backed by product direction plus Gonka -deployment metadata checked on 2026-06-23: +The current model contract is backed by product direction plus Kilo +compatibility evidence: -- Gonka deployment model args list Kimi K2.6 and Qwen3 235B A22B Instruct 2507 - FP8 with `--max-model-len 240000` -- Gonka deployment model args list `minimaxai/minimax-m2.7` with - `--max-model-len 180000` -- the model card documents OpenAI-compatible chat completions access through - Moonshot's API -- the API docs describe the supported request path as `POST /v1/chat/completions` -- the package writes `limit.output = 8192` for validated catalog entries as - the installer-managed Kilo compatibility clamp because Kilo `7.2.0` requires - a numeric output limit in custom model config -- the package keeps the same installer-managed `limit.output = 8192` - compatibility clamp for every validated catalog entry +- model availability is resolved from GonkaGate `GET /v1/models` after safe + API-key intake +- the API docs describe the supported request path as + `POST /v1/chat/completions` +- the package writes `limit.output = 8192` for fetched catalog entries as the + installer-managed Kilo compatibility clamp because Kilo `7.2.0` requires a + numeric output limit in custom model config - npm registry metadata checked on 2026-04-29 showed `@kilocode/cli` patch releases in the `7.2.x` line, including `7.2.14`, with both `kilo` and `kilocode` binaries still exposed by the wrapper package @@ -62,22 +53,21 @@ The repository must continue to avoid these claims: - no claim that GonkaGate `responses` transport works today - no claim that real-path Kilo verification is the production default - no claim that native Windows production support is proven -- no claim that every future Kilo behavior or live GonkaGate model is - proven just because the current curated default is shipped +- no claim that every future Kilo behavior or live GonkaGate model is proven + beyond fetching and writing the catalog shape ## Remaining Follow-Up Items These items still benefit from fresh proof or human review outside this repository-only pass: -- fresh live GonkaGate/Kilo smoke evidence when the selected public model or - Kilo baseline changes +- fresh live GonkaGate/Kilo smoke evidence when Kilo's model config + requirements or transport behavior changes - native Windows oracle-safety proof on a real runner or equivalent native VM - product/legal approval for public publishing under `@gonkagate/kilo-setup` Those items do not change the current package contract: the publishable surface -remains the minimum Kilo `7.2.0` floor without an upper version bound, the -validated GonkaGate chat-completions catalog with Kimi as the recommended -default, Qwen3 and MiniMax as additional validated entries, and the current -non-Windows verification policy documented above. +remains the minimum Kilo `7.2.0` floor without an upper version bound, +authenticated GonkaGate `/v1/models` discovery for the chat-completions +catalog, and the current non-Windows verification policy documented above. diff --git a/docs/security.md b/docs/security.md index f5dfc68..d6b080f 100644 --- a/docs/security.md +++ b/docs/security.md @@ -54,6 +54,7 @@ proven. that sandbox up after verification - do not claim real-path Kilo verification as the production default -The current runtime already rejects plain `--api-key`, keeps stock public runs -blocked before writes when no validated curated model exists, and redacts -secret-bearing text on user-facing error paths. +The current runtime already rejects plain `--api-key`, fetches the model +catalog only after safe API-key intake, blocks before writes when `/v1/models` +does not return usable models, and redacts secret-bearing text on user-facing +error paths. diff --git a/docs/specs/kilo-setup-prd/compatibility-spike-notes.md b/docs/specs/kilo-setup-prd/compatibility-spike-notes.md index 4abbb69..0d962bc 100644 --- a/docs/specs/kilo-setup-prd/compatibility-spike-notes.md +++ b/docs/specs/kilo-setup-prd/compatibility-spike-notes.md @@ -2,6 +2,9 @@ Date: April 14, 2026 +Current model availability comes from authenticated GonkaGate `GET /v1/models`; +model ids below are historical evidence examples, not the runtime catalog. + These notes preserve redacted evidence for the GonkaGate Kilo setup PRD. They do not contain raw `kilo debug config` output or real secrets. diff --git a/docs/specs/kilo-setup-prd/implementation-plan.md b/docs/specs/kilo-setup-prd/implementation-plan.md index 33cb470..38270b9 100644 --- a/docs/specs/kilo-setup-prd/implementation-plan.md +++ b/docs/specs/kilo-setup-prd/implementation-plan.md @@ -1,5 +1,10 @@ # Implementation Plan: GonkaGate Kilo Setup +Current repository behavior supersedes the draft model-catalog tasks below: +the installer now fetches authenticated `GET /v1/models` after safe API-key +intake and uses that live response for model choices, config writes, and +`--model` validation. + ## Overview This plan breaks the draft PRD into ordered, verifiable implementation tasks. @@ -317,7 +322,7 @@ diagnostics for the user and for machine-readable JSON output. - [ ] Detect `KILO_CONFIG`, `KILO_CONFIG_DIR`, home-level directory, and `KILO_CONFIG_CONTENT` blockers. -- [ ] Detect provider shape mismatch, missing curated model entry, provider +- [ ] Detect provider shape mismatch, missing selected model entry, provider allow/deny, provider whitelist/blacklist, and secret-binding provenance mismatch. - [ ] Treat `disabled_providers` as stronger than `enabled_providers` when both @@ -422,14 +427,13 @@ sandbox and installer-owned targets. ## Task 12: Enable Interactive Setup **Description:** Replace scaffold-only execution with an interactive install -flow that detects Kilo, shows the installed version, offers curated model -choices, recommends scope, accepts a hidden secret, writes config, verifies, and +flow that detects Kilo, shows the installed version, fetches live model choices from `/v1/models`, recommends scope, accepts a hidden secret, writes config, verifies, and ends by sending the user back to plain `kilo`. **Acceptance criteria:** - [ ] Interactive flow shows the selected Kilo command and version. -- [ ] Model picker shows only validated curated choices. +- [ ] Model picker shows fetched `/v1/models` choices. - [ ] Project scope is recommended inside a git repository and user scope outside a git repository. - [ ] Prompt text explains Kilo-specific scope effects in user language. @@ -626,8 +630,7 @@ acceptance criteria before claiming production installer support. ## Open Questions -- What is the proven numeric `limit.output` for - `qwen/qwen3-235b-a22b-instruct-2507-fp8`? +- What numeric `limit.output` policy should fetched model entries use? - Does the live GonkaGate/Kilo `chat/completions` smoke pass tool-call behavior for the selected model? - Will product/legal approve `@gonkagate/kilo-setup` as the public package diff --git a/docs/specs/kilo-setup-prd/spec.md b/docs/specs/kilo-setup-prd/spec.md index 524dd60..a28742b 100644 --- a/docs/specs/kilo-setup-prd/spec.md +++ b/docs/specs/kilo-setup-prd/spec.md @@ -5,6 +5,11 @@ This document does not describe shipped behavior in this repository yet. Last upstream check: April 14, 2026. +Current repository behavior supersedes the draft model-catalog portions below: +the installer now fetches authenticated `GET /v1/models` after safe API-key +intake and uses that live response for model choices, config writes, and +`--model` validation. + ## Source Baseline The original investigation request used the literal phrase `kilo-code cli`. @@ -126,7 +131,7 @@ The tool: 1. validates that local Kilo CLI is installed as `kilo` or `kilocode` 2. identifies the installed Kilo CLI version and channel 3. refuses versions without a supported compatibility profile -4. shows only curated GonkaGate model choices +4. fetches live GonkaGate model choices from /v1/models 5. lets the user choose `user` or `project` scope 6. accepts the GonkaGate API key through safe inputs only 7. writes the minimum safe Kilo config layers @@ -142,7 +147,7 @@ Interactive mode should: - detect local `kilo` - display installed Kilo CLI version -- show the curated model picker even if only one model is currently validated +- show the live model picker even if only one model is currently validated - recommend `project` scope inside a git repository - recommend `user` scope outside a git repository - explain the Kilo-specific scope effect in user language @@ -162,7 +167,7 @@ In scope: - support for `kilo` as the primary command and `kilocode` as a fallback alias - Kilo CLI version detection - hidden or automation-safe GonkaGate secret input -- curated GonkaGate model picker +- live GonkaGate model picker - `user` and `project` activation scopes - managed user secret storage - managed install-state storage @@ -182,7 +187,6 @@ Out of scope: - writing secrets to repository-local files - writing directly to Kilo's `auth.json` - depending on Kilo Gateway or Kilo account login -- runtime `/v1/models` discovery as the primary onboarding UX - arbitrary custom base URLs - arbitrary custom model IDs - claiming GonkaGate `responses` support before validation @@ -197,7 +201,7 @@ Out of scope: - canonical base URL: `https://api.gonkagate.com/v1` - current supported transport: `/v1/chat/completions` - `/v1/responses` is not supported today unless separately revalidated -- setup must expose only curated, Kilo-validated model choices in the public +- setup must expose the authenticated GonkaGate `/v1/models` choices in the public setup flow - the current planned model inherited from the OpenCode setup baseline is: `qwen/qwen3-235b-a22b-instruct-2507-fp8` @@ -572,7 +576,7 @@ Evidence: `api.url` is null. Model listing alone is not a sufficient URL verifier. - A local fake OpenAI-compatible `kilo run` smoke proved that `options.baseURL = http://127.0.0.1:/v1` sends a streaming POST to - `/v1/chat/completions` with the curated upstream model id and a Bearer header. + `/v1/chat/completions` with the selected upstream model id and a Bearer header. - The same fake smoke proved that writing a full `.../chat/completions` URL produces a doubled `/chat/completions/chat/completions` path. @@ -592,7 +596,7 @@ also observed `limit.input` preserved in raw resolved config but absent from config and keep it only as future-compatible registry metadata if GonkaGate publishes it. -The curated model registry must be able to carry: +The live model catalog boundary must be able to carry: - visible model key - upstream model id @@ -686,7 +690,7 @@ The installer must detect and report: - provider-level `whitelist` that excludes the selected GonkaGate model - provider-level `blacklist` that includes the selected GonkaGate model - provider block shape mismatches for `gonkagate` -- missing curated model entry +- missing selected model entry - secret-binding provenance mismatches `disabled_providers` should be treated as stronger than `enabled_providers` @@ -786,7 +790,7 @@ resolver must check: - provider adapter package - base URL - transport-relevant shape -- curated model entry +- selected model entry - provider allow/deny gating - provider whitelist/blacklist gating @@ -1024,9 +1028,7 @@ Production must not be claimed until: - the provider config shape is proven against a live GonkaGate/Kilo `chat/completions` smoke path, including tool-call behavior, using only an already-set `GONKAGATE_API_KEY` or a hidden prompt -- the real numeric `limit.output` value is known for - `qwen/qwen3-235b-a22b-instruct-2507-fp8`, or that model is excluded from - validated defaults +- the generic numeric `limit.output` value is chosen for fetched model entries - `limit.input` is excluded from production runtime config - the local resolver, sandboxed Kilo oracle, redaction path, rollback, and XDG-isolated verification strategy are implemented and tested @@ -1133,7 +1135,7 @@ Production release is ready when: These are not post-production phases for the setup utility. They require a changed external contract, an additional audited Kilo compatibility profile, or -new validated GonkaGate model data before they can become production +new GonkaGate model metadata requirements before they can become production requirements: - GonkaGate `/v1/responses`, because GonkaGate currently supports @@ -1143,7 +1145,6 @@ requirements: - enterprise-aware managed config policies, because production currently blocks higher-precedence managed secret bindings rather than treating them as supported enterprise ownership -- additional GonkaGate models, because production exposes only curated, - Kilo-validated model choices +- new model metadata fields, because production currently trusts only `/v1/models` id and name - real-path Kilo verification, because the production verifier strategy is the XDG-isolated sandbox oracle diff --git a/docs/troubleshooting.md b/docs/troubleshooting.md index 93c9c85..7cd0892 100644 --- a/docs/troubleshooting.md +++ b/docs/troubleshooting.md @@ -1,29 +1,20 @@ # Troubleshooting -## The CLI Says No Validated Models Are Available +## The CLI Says No Usable Models Were Returned -That is no longer the expected stock public result. +The installer fetches models from authenticated +`GET https://api.gonkagate.com/v1/models` after collecting the GonkaGate API +key. -The published package now ships -`moonshotai/Kimi-K2.6` as the recommended validated curated default with -installer-managed `limit.output = 8192`. - -If you still see `validated_models_unavailable`, you are likely running a -stale build, a locally modified package, or a fork whose curated registry does -not include the published default. - -If you see a config error mentioning -`provider.gonkagate.models.kimi-k2.6.limit.output`, -you are likely running a stale build that predates the required Kilo 7.2.0 -output-limit fix. +If you see `validated_models_unavailable`, the response was unavailable, +empty, or not shaped like `{ data: [{ id: string, name?: string }] }`. +Confirm the API key is valid and rerun setup. ## Why Does `/models` Show Too Few GonkaGate Models? -Current builds write the validated GonkaGate chat-completions catalog into +Current builds write the fetched GonkaGate chat-completions catalog into `provider.gonkagate.models`, so Kilo's OpenCode-style `/models` picker should -show `moonshotai/Kimi-K2.6`, -`qwen/qwen3-235b-a22b-instruct-2507-fp8`, and -`minimaxai/minimax-m2.7`. +show every model returned by `/v1/models`. If `/models` still shows fewer GonkaGate models, rerun the installer from a current build so the user-level `provider.gonkagate` definition is refreshed. diff --git a/docs/user-guide.md b/docs/user-guide.md index 6336fae..b6db52c 100644 --- a/docs/user-guide.md +++ b/docs/user-guide.md @@ -23,14 +23,10 @@ Today the public package is intentionally narrow: - minimum Kilo compatibility floor: `@kilocode/cli >=7.2.0` - current transport target: `chat/completions` -- current curated default: - `moonshotai/Kimi-K2.6` -- validated GonkaGate catalog for Kilo's OpenCode-style `/models` picker: - `moonshotai/Kimi-K2.6`, - `qwen/qwen3-235b-a22b-instruct-2507-fp8`, and - `minimaxai/minimax-m2.7` -- managed model limits: `limit.output = 8192` for all validated entries; - `limit.context = 240000` for Kimi/Qwen and `180000` for MiniMax +- model availability comes from authenticated `GET /v1/models` +- Kilo's OpenCode-style `/models` picker receives every fetched GonkaGate + model +- managed model limit: `limit.output = 8192` for each fetched model entry - no native Windows production claim yet ## Before You Run It @@ -61,15 +57,16 @@ The installer will then: 1. Detect `kilo`, or fall back to `kilocode`. 2. Check the minimum supported Kilo floor. -3. Offer the curated GonkaGate model choice. -4. Write the validated GonkaGate provider model catalog for Kilo's `/models` +3. Prompt for the GonkaGate API key using a hidden input. +4. Fetch the live GonkaGate model catalog from `/v1/models`. +5. Offer the live GonkaGate model choices. +6. Write the fetched GonkaGate provider model catalog for Kilo's `/models` picker. -5. Choose the recommended scope automatically: +7. Choose the recommended scope automatically: - inside a git repository: `project` - outside a repository: `user` -6. Prompt for the GonkaGate API key using a hidden input. -7. Write the managed config and verify the result. -8. Return you to normal `kilo` usage. +8. Write the managed config and verify the result. +9. Return you to normal `kilo` usage. Interactive reruns ask about scope only when the previous installer-managed scope differs from the new recommendation. @@ -162,7 +159,7 @@ Use `--json` when another tool needs a machine-readable result. What they are for: -- `--model`: choose a curated GonkaGate model key explicitly +- `--model`: choose a GonkaGate model id returned by `/v1/models` explicitly - `--scope`: choose `user` or `project` without an interactive prompt - `--cwd`: change the directory used for project-scope path resolution - `--api-key-stdin`: read the API key from standard input diff --git a/scripts/run-kilo-oracle-proof.mjs b/scripts/run-kilo-oracle-proof.mjs index 128b651..69dd264 100644 --- a/scripts/run-kilo-oracle-proof.mjs +++ b/scripts/run-kilo-oracle-proof.mjs @@ -9,6 +9,7 @@ import { buildManagedProviderConfig, formatKiloModelRef, } from "../dist/install/managed-provider-config.js"; +import { createInstallModelCatalog } from "../dist/install/model-catalog.js"; import { resolveManagedPaths } from "../dist/install/paths.js"; import { resolveDurableLocalKiloConfig } from "../dist/install/verify-layers.js"; import { tryParseJsoncObject } from "../dist/install/jsonc.js"; @@ -38,6 +39,18 @@ const managedPaths = resolveManagedPaths( ); const selectedModelKey = "qwen3-235b-a22b-instruct-2507-fp8"; const selectedModelRef = formatKiloModelRef(selectedModelKey); +const fixtureModelCatalog = createInstallModelCatalog([ + { + adapterPackage: "@ai-sdk/openai-compatible", + displayName: "Oracle Proof Model", + key: selectedModelKey, + limits: { context: 240000, output: 8192 }, + modelId: selectedModelKey, + recommended: true, + transport: "chat_completions", + validationStatus: "validated", + }, +]); try { await seedFixtureFiles(); @@ -133,7 +146,10 @@ async function seedFixtureFiles() { `${JSON.stringify( { provider: { - gonkagate: buildManagedProviderConfig(selectedModelKey), + gonkagate: buildManagedProviderConfig( + selectedModelKey, + fixtureModelCatalog, + ), }, }, null, @@ -221,6 +237,7 @@ async function executeOracleInvocation(invocation, dependencies) { exitCode: result.exitCode, ok: false, signal: result.signal, + stderr: redactSensitiveOutput(result.stderr), stderrLength: result.stderr.length, }; } @@ -244,7 +261,11 @@ function formatOracleFailure(label, attempt) { return `${label} invocation ${attempt.command} ${attempt.args.join(" ")} threw ${String(attempt.error)}.`; } - return `${label} invocation ${attempt.command} ${attempt.args.join(" ")} failed with exit code ${attempt.exitCode}${attempt.signal ? ` and signal ${attempt.signal}` : ""}. stderr length=${attempt.stderrLength}.`; + return `${label} invocation ${attempt.command} ${attempt.args.join(" ")} failed with exit code ${attempt.exitCode}${attempt.signal ? ` and signal ${attempt.signal}` : ""}. stderr length=${attempt.stderrLength}. stderr=${attempt.stderr}`; +} + +function redactSensitiveOutput(value) { + return value.replace(/gp-[A-Za-z0-9]+/gu, "gp-[redacted]"); } function collectRealPathCandidates() { diff --git a/src/cli/parse.ts b/src/cli/parse.ts index 289fc46..874ac1d 100644 --- a/src/cli/parse.ts +++ b/src/cli/parse.ts @@ -54,8 +54,8 @@ function createProgram(output?: ProgramOutput): Command { .description("Configure Kilo to use GonkaGate.") .addOption( new Option( - "--model ", - "Choose a curated GonkaGate model key.", + "--model ", + "Choose a GonkaGate model id returned by /v1/models.", ), ) .addOption( @@ -110,7 +110,7 @@ Examples: printf '%s' "$${GONKAGATE_SECRET_ENV_VAR}" | ${CONTRACT_METADATA.publicEntrypoint} --api-key-stdin --scope project --yes --json Runtime status: - The installer runtime is shipped for Kilo and currently defaults to the validated Kimi K2.6 profile for @kilocode/cli >=7.2.0 installs. + The installer runtime fetches GonkaGate models from ${GONKAGATE_BASE_URL}/models after API-key intake for @kilocode/cli >=7.2.0 installs. Kilo contract anchors: - Primary command: ${KILO_PRIMARY_COMMAND} diff --git a/src/constants/contract.ts b/src/constants/contract.ts index 3187c1a..d80903d 100644 --- a/src/constants/contract.ts +++ b/src/constants/contract.ts @@ -5,7 +5,7 @@ export const CONTRACT_METADATA = { packageName: "@gonkagate/kilo-setup", publicEntrypoint: "npx @gonkagate/kilo-setup", publicState: - "Installer runtime is shipped with a validated curated GonkaGate chat-completions catalog for @kilocode/cli >=7.2.0 installs, with Kimi K2.6 as the recommended default and installer-managed limit.output = 8192 for compatibility.", + "Installer runtime fetches the GonkaGate chat-completions model catalog from /v1/models after safe API-key intake for @kilocode/cli >=7.2.0 installs.", secondaryBinName: "gonkagate-kilo", upstreamKilo: { checkedAt: "2026-04-29", diff --git a/src/constants/models.ts b/src/constants/models.ts deleted file mode 100644 index 55dab3e..0000000 --- a/src/constants/models.ts +++ /dev/null @@ -1,272 +0,0 @@ -import type { JsonObject, JsonValue } from "../json.js"; - -export const CURATED_MODEL_TRANSPORTS = Object.freeze([ - "chat_completions", - "responses", -] as const); - -export type CuratedModelTransport = (typeof CURATED_MODEL_TRANSPORTS)[number]; -export type CuratedModelValidationStatus = "planned" | "validated"; - -export type CuratedModelJsonValue = JsonValue; - -export interface CuratedModelCompatibility { - modelHeaders?: Readonly>; - modelOptions?: Readonly; - notes?: readonly string[]; - providerOptions?: Readonly; -} - -export interface CuratedModelLimits { - context?: number; - output?: number; -} - -export interface CuratedModelDefinition { - adapterPackage: string; - displayName: string; - limits?: CuratedModelLimits; - modelId: string; - recommended: boolean; - runtimeCompatibility?: CuratedModelCompatibility; - transport: CuratedModelTransport; - validationStatus: CuratedModelValidationStatus; -} - -export interface CuratedModelRegistry { - readonly [key: string]: CuratedModelDefinition; -} - -export type CuratedModelRecord = - CuratedModelDefinition & { - key: TKey; - }; - -type CuratedModelKeyOf = Extract< - keyof TRegistry, - string ->; -type CuratedModelRecordFor< - TRegistry extends CuratedModelRegistry, - TKey extends CuratedModelKeyOf = CuratedModelKeyOf, -> = TRegistry[TKey] & { - key: TKey; -}; -type ValidatedCuratedModelRecordFor< - TRegistry extends CuratedModelRegistry, - TKey extends CuratedModelKeyOf = CuratedModelKeyOf, -> = Extract< - CuratedModelRecordFor, - { 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[]; - recommendedProductionDefaultModel: - | RecommendedValidatedCuratedModelRecordFor - | undefined; - validatedModelKeys: readonly ValidatedCuratedModelRecordFor["key"][]; - validatedModels: readonly ValidatedCuratedModelRecordFor[]; -} - -export const CURATED_MODEL_REGISTRY = Object.freeze({ - "kimi-k2.6": { - adapterPackage: "@ai-sdk/openai-compatible", - displayName: "Kimi K2.6", - limits: { - context: 240000, - output: 8192, - }, - modelId: "moonshotai/Kimi-K2.6", - recommended: true, - runtimeCompatibility: { - notes: [ - "Gonka deployment metadata lists Kimi K2.6 with a 240,000 token context window and OpenAI-compatible chat completions access.", - "The installer writes limit.output = 8192 as a conservative Kilo 7.2.0 compatibility clamp for this default.", - ], - }, - transport: "chat_completions", - validationStatus: "validated", - }, - "qwen3-235b-a22b-instruct-2507-fp8": { - adapterPackage: "@ai-sdk/openai-compatible", - displayName: "Qwen3 235B A22B Instruct 2507 FP8", - limits: { - context: 240000, - output: 8192, - }, - modelId: "qwen/qwen3-235b-a22b-instruct-2507-fp8", - recommended: false, - runtimeCompatibility: { - notes: [ - "Gonka deployment metadata lists this model as available with a 240,000 token context window.", - "Official GonkaGate OpenClaw provider guidance lists this model with maxTokens 8192.", - "Kilo 7.2.0 rejects custom model entries without a numeric limit.output value, so the installer writes limit.output = 8192 for this validated default.", - ], - }, - transport: "chat_completions", - validationStatus: "validated", - }, - "minimax-m2.7": { - adapterPackage: "@ai-sdk/openai-compatible", - displayName: "MiniMax M2.7", - limits: { - context: 180000, - output: 8192, - }, - modelId: "minimaxai/minimax-m2.7", - recommended: false, - runtimeCompatibility: { - notes: [ - "Gonka deployment metadata lists minimaxai/minimax-m2.7 as MiniMax M2.7 with a 180,000 token context window.", - "The installer writes limit.output = 8192 as the existing conservative Kilo 7.2.0 compatibility clamp for validated catalog entries.", - ], - }, - transport: "chat_completions", - validationStatus: "validated", - }, -} as const satisfies CuratedModelRegistry); - -function toCuratedModelRecord< - TKey extends string, - TDefinition extends CuratedModelDefinition, ->(key: TKey, definition: TDefinition): TDefinition & { key: TKey } { - return { - ...definition, - key, - }; -} - -export function isValidatedModel< - TModel extends { validationStatus: CuratedModelValidationStatus }, ->(model: TModel): model is Extract { - return model.validationStatus === "validated"; -} - -export function isRecommendedModel( - model: TModel, -): model is Extract { - return model.recommended; -} - -export function createCuratedModelIndex( - registry: TRegistry, -): CuratedModelIndex { - type RegistryKey = CuratedModelKeyOf; - type RegistryModel = CuratedModelRecordFor; - type ValidatedRegistryModel = ValidatedCuratedModelRecordFor; - type RecommendedValidatedRegistryModel = - RecommendedValidatedCuratedModelRecordFor; - - const modelKeys = Object.keys(registry) as RegistryKey[]; - const models: RegistryModel[] = []; - const validatedModels: ValidatedRegistryModel[] = []; - const validatedModelKeys: ValidatedRegistryModel["key"][] = []; - const recommendedProductionDefaultModels: RecommendedValidatedRegistryModel[] = - []; - - for (const key of modelKeys) { - const model = toCuratedModelRecord(key, registry[key]); - models.push(model); - - if (!isValidatedModel(model)) { - continue; - } - - validatedModels.push(model); - validatedModelKeys.push(model.key); - - if (isRecommendedModel(model)) { - recommendedProductionDefaultModels.push(model); - } - } - - if (recommendedProductionDefaultModels.length > 1) { - throw new Error( - "Curated model registry must not expose more than one recommended validated production default.", - ); - } - - return { - modelKeys: Object.freeze(modelKeys), - models: Object.freeze(models), - recommendedProductionDefaultModel: recommendedProductionDefaultModels[0], - validatedModelKeys: Object.freeze(validatedModelKeys), - validatedModels: Object.freeze(validatedModels), - }; -} - -type DefaultCuratedModelRegistry = typeof CURATED_MODEL_REGISTRY; - -export type CuratedModelKey = CuratedModelKeyOf; -export type CuratedModel = CuratedModelDefinition & { key: CuratedModelKey }; -export type CuratedModelByKey = - CuratedModelRecordFor; -export type ValidatedCuratedModel = CuratedModelDefinition & { - key: CuratedModelKey; - validationStatus: "validated"; -}; -export type RecommendedProductionDefaultCuratedModel = ValidatedCuratedModel & { - recommended: true; -}; - -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 as readonly ValidatedCuratedModel[]; -} - -export function getValidatedModelKeys(): readonly ValidatedCuratedModel["key"][] { - return DEFAULT_CURATED_MODEL_INDEX.validatedModelKeys as readonly ValidatedCuratedModel["key"][]; -} - -export function getRecommendedProductionDefaultModel(): - | RecommendedProductionDefaultCuratedModel - | undefined { - return DEFAULT_CURATED_MODEL_INDEX.recommendedProductionDefaultModel as - | RecommendedProductionDefaultCuratedModel - | undefined; -} diff --git a/src/install/README.md b/src/install/README.md index cc1c04f..801482b 100644 --- a/src/install/README.md +++ b/src/install/README.md @@ -1,25 +1,23 @@ # `src/install/` -This directory now contains the shipped Kilo installer runtime. - -Start here: +This directory contains the shipped Kilo installer runtime. Start here: - `index.ts` orchestrates the end-to-end install flow - `context.ts`, `selection.ts`, and `session.ts` resolve Kilo, workspace, - validated-model choice, and scope summary -- `deps.ts` keeps command, filesystem, prompt, input, clock, and model-catalog - seams testable -- `secrets.ts`, `scope.ts`, and `state.ts` own managed secret/config/install- + live-model choice, and scope summary +- `deps.ts` keeps command, filesystem, HTTP, prompt, input, clock, and + model-catalog seams testable +- `secrets.ts`, `scope.ts`, and `state.ts` own managed secret/config/install state writes -- `managed-provider-config.ts` writes the validated GonkaGate - chat-completions model catalog into the Kilo/OpenCode-compatible provider - shape while keeping activation in the top-level `model` setting +- `managed-provider-config.ts` writes the fetched GonkaGate chat-completions + model catalog into the Kilo/OpenCode-compatible provider shape while keeping + activation in the top-level `model` setting - `managed-write-transaction.ts` and `rollback.ts` restore installer-owned writes when durable verification fails - `verify-effective.ts`, `verify-layers.ts`, and related helpers implement the durable/current-session Kilo proof path with redacted diagnostics -The current stock runtime ships a validated curated Kimi K2.6 default plus the -validated Qwen3 235B A22B Instruct 2507 FP8 and MiniMax M2.7 catalog entries. -Keep Kilo-specific behavior here and do not import OpenCode env vars or config -targets as runtime defaults. +The stock runtime fetches `GET https://api.gonkagate.com/v1/models` after safe +API-key intake and uses that live response as the installer model catalog. +Keep Kilo-specific behavior explicit and do not add OpenCode env vars, config +targets, or runtime defaults. diff --git a/src/install/contracts.ts b/src/install/contracts.ts index f6035e7..11cea5d 100644 --- a/src/install/contracts.ts +++ b/src/install/contracts.ts @@ -3,11 +3,11 @@ import type { EffectiveConfigVerificationMismatch, EffectiveConfigVerificationTarget, } from "./contracts/effective-config.js"; -import type { - CuratedModelKey, - CuratedModelTransport, -} from "../constants/models.js"; import type { GONKAGATE_PROVIDER_ID } from "../constants/gateway.js"; +import type { + InstallModelKey, + InstallModelTransport, +} from "./model-catalog.js"; import type { InstallErrorCode } from "./errors.js"; export type InstallScope = "project" | "user"; @@ -73,7 +73,7 @@ interface InstallFlowResultBase { export interface InstallFlowSelectionSummary { kilo: SupportedKiloSummary; modelDisplayName: string; - modelKey: CuratedModelKey; + modelKey: InstallModelKey; modelRef: string; scope: InstallScope; } @@ -81,7 +81,7 @@ export interface InstallFlowSelectionSummary { export interface InstallFlowProgress { kilo?: SupportedKiloSummary; modelDisplayName?: string; - modelKey?: CuratedModelKey; + modelKey?: InstallModelKey; modelRef?: string; scope?: InstallScope; } @@ -96,7 +96,7 @@ export interface BlockedInstallResult extends InstallFlowResultBase { | "scope_selection_required"; kilo?: BlockedKiloSummary | SupportedKiloSummary; modelDisplayName?: string; - modelKey?: CuratedModelKey; + modelKey?: InstallModelKey; modelRef?: string; ok: false; mismatches?: readonly EffectiveConfigVerificationMismatch[]; @@ -133,7 +133,7 @@ export interface InstalledInstallResult providerId: typeof GONKAGATE_PROVIDER_ID; scope: InstallScope; status: "installed"; - transport: CuratedModelTransport; + transport: InstallModelTransport; } export type InstallFlowResult = diff --git a/src/install/contracts/effective-config.ts b/src/install/contracts/effective-config.ts index 0a941de..99c6143 100644 --- a/src/install/contracts/effective-config.ts +++ b/src/install/contracts/effective-config.ts @@ -1,7 +1,7 @@ import type { - CuratedModelKey, - CuratedModelTransport, -} from "../../constants/models.js"; + InstallModelKey, + InstallModelTransport, +} from "../model-catalog.js"; import type { RedactedDiagnosticData, RedactedDiagnosticValue, @@ -50,10 +50,10 @@ export interface EffectiveConfigVerificationLayerSnapshot { } export interface EffectiveConfigVerificationTarget { - modelKey: CuratedModelKey; + modelKey: InstallModelKey; modelRef: string; providerId: string; - transport: CuratedModelTransport | "chat/completions"; + transport: InstallModelTransport | "chat/completions"; } export type EffectiveConfigDiagnosticData = RedactedDiagnosticData; diff --git a/src/install/contracts/install-state.ts b/src/install/contracts/install-state.ts index 07c8de8..a9dcbd4 100644 --- a/src/install/contracts/install-state.ts +++ b/src/install/contracts/install-state.ts @@ -1,8 +1,8 @@ -import type { - CuratedModelKey, - CuratedModelTransport, -} from "../../constants/models.js"; import type { InstallScope } from "../contracts.js"; +import type { + InstallModelKey, + InstallModelTransport, +} from "../model-catalog.js"; export interface ManagedInstallStateRecord { compatibilityAuditVersion: string; @@ -10,12 +10,12 @@ export interface ManagedInstallStateRecord { project?: string; user: string; }; - currentTransport: CuratedModelTransport; + currentTransport: InstallModelTransport; installerPackageName: string; installerVersion: string; kiloCommand: string; kiloVersion: string; lastDurableSetupAt?: string; - selectedModelKey: CuratedModelKey; + selectedModelKey: InstallModelKey; selectedScope: InstallScope; } diff --git a/src/install/contracts/managed-config.ts b/src/install/contracts/managed-config.ts index 6371e4e..211856a 100644 --- a/src/install/contracts/managed-config.ts +++ b/src/install/contracts/managed-config.ts @@ -1,5 +1,5 @@ -import type { CuratedModelKey } from "../../constants/models.js"; import type { JsonObject, JsonValue } from "../../json.js"; +import type { InstallModelCatalog, InstallModelKey } from "../model-catalog.js"; import type { ManagedPaths } from "../paths.js"; import type { ManagedArtifactWriteResult } from "./managed-artifact.js"; @@ -85,8 +85,9 @@ export type ManagedConfigPlan = export interface ScopeWriteRequest { managedPaths: ManagedPaths; - model: CuratedModelKey; - previousManagedModelKey?: CuratedModelKey; + model: InstallModelKey; + modelCatalog: InstallModelCatalog; + previousManagedModelKey?: InstallModelKey; projectRoot: string; scope: ManagedConfigScope; } diff --git a/src/install/deps.ts b/src/install/deps.ts index d8bbfbc..3e621f5 100644 --- a/src/install/deps.ts +++ b/src/install/deps.ts @@ -15,6 +15,7 @@ import { password, select } from "@inquirer/prompts"; import writeFileAtomic from "write-file-atomic"; import { createDefaultInstallModelCatalog, + type InstallHttpClient, type InstallModelCatalog, } from "./model-catalog.js"; @@ -76,6 +77,8 @@ export interface InstallInput { readStdin(): Promise; } +export type { InstallHttpClient }; + export interface InstallSelectChoice { description?: string; label: string; @@ -120,6 +123,7 @@ export interface InstallDependencies { clock: InstallClock; commands: InstallCommandRunner; fs: InstallFs; + http: InstallHttpClient; input: InstallInput; models: InstallModelCatalog; prompts: InstallPrompts; @@ -130,6 +134,7 @@ export interface CreateInstallDependenciesOverrides { clock?: Partial; commands?: Partial; fs?: Partial; + http?: Partial; input?: Partial; models?: Partial; prompts?: Partial; @@ -268,6 +273,33 @@ async function readStdin(): Promise { }); } +async function fetchJson( + url: string, + options: Parameters[1] = {}, +): Promise>> { + const response = await fetch(url, { + headers: options.headers, + method: options.method ?? "GET", + }); + let body: unknown; + + try { + body = (await response.json()) as unknown; + } catch { + body = undefined; + } + + return { + body, + ok: response.ok, + status: response.status, + }; +} + +const DEFAULT_INSTALL_HTTP: InstallHttpClient = { + fetchJson, +}; + async function readSecret(message: string): Promise { return await password({ mask: "*", @@ -327,6 +359,7 @@ export function createNodeInstallDependencies( clock: { ...DEFAULT_INSTALL_CLOCK, ...overrides.clock }, commands: { ...DEFAULT_INSTALL_COMMANDS, ...overrides.commands }, fs: { ...DEFAULT_INSTALL_FS, ...overrides.fs }, + http: { ...DEFAULT_INSTALL_HTTP, ...overrides.http }, input: { ...DEFAULT_INSTALL_INPUT, ...overrides.input }, models: { ...DEFAULT_INSTALL_MODELS, ...overrides.models }, prompts: { ...DEFAULT_INSTALL_PROMPTS, ...overrides.prompts }, diff --git a/src/install/effective-config-policy.ts b/src/install/effective-config-policy.ts index 07bcf78..f6193ac 100644 --- a/src/install/effective-config-policy.ts +++ b/src/install/effective-config-policy.ts @@ -1,7 +1,3 @@ -import { - type CuratedModelKey, - getCuratedModelByKey, -} from "../constants/models.js"; import { CURRENT_TRANSPORT_TARGET, GONKAGATE_PROVIDER_ID, @@ -12,6 +8,7 @@ import { buildManagedProviderConfig, GONKAGATE_SECRET_FILE_REFERENCE, } from "./managed-provider-config.js"; +import type { InstallModelCatalog, InstallModelKey } from "./model-catalog.js"; import type { EffectiveConfigValueCheck } from "./verification-mismatches.js"; export interface SecretBindingVerificationPolicy { @@ -36,12 +33,13 @@ const PROVIDER_REASON_RULES = Object.freeze({ } as const satisfies Record); export function createResolvedConfigVerificationPolicy( - modelKey: CuratedModelKey, + modelKey: InstallModelKey, + catalog: InstallModelCatalog, ): ResolvedConfigVerificationPolicy { - const model = getCuratedModelByKey(modelKey); + const model = catalog.getModelByKey(modelKey); if (model === undefined) { - throw new Error(`Unsupported curated model key: ${modelKey}`); + throw new Error(`Unsupported GonkaGate model id: ${modelKey}`); } const target: EffectiveConfigVerificationTarget = { @@ -51,7 +49,7 @@ export function createResolvedConfigVerificationPolicy( transport: CURRENT_TRANSPORT_TARGET as EffectiveConfigVerificationTarget["transport"], }; - const expectedProviderConfig = buildManagedProviderConfig(modelKey); + const expectedProviderConfig = buildManagedProviderConfig(modelKey, catalog); return { target, diff --git a/src/install/errors.ts b/src/install/errors.ts index 75e33d9..e95a993 100644 --- a/src/install/errors.ts +++ b/src/install/errors.ts @@ -164,9 +164,9 @@ const INSTALL_ERROR_MESSAGE_FACTORIES: { ? "Non-interactive setup inside a git repository requires --scope or --yes so the installer can choose between user and project activation safely." : "Non-interactive setup requires --scope or --yes so the installer can confirm the activation scope safely.", unsupported_model_key: (details) => - `The model key ${details.modelKey} is not a validated curated GonkaGate Kilo option.`, + `The model id ${details.modelKey} was not returned by GonkaGate /v1/models.`, validated_models_unavailable: () => - "No validated curated GonkaGate Kilo models are currently available in this package build.", + "No usable GonkaGate Kilo models were returned by /v1/models.", }; export class InstallError extends Error { diff --git a/src/install/index.ts b/src/install/index.ts index 8d2744d..c8d0eda 100644 --- a/src/install/index.ts +++ b/src/install/index.ts @@ -27,6 +27,7 @@ import { verifyEffectiveKiloConfig, } from "./verify-effective.js"; import type { ManagedArtifactRollbackAction } from "./contracts/managed-artifact.js"; +import type { ResolvedSecretInput } from "./secrets.js"; import type { EffectiveConfigVerificationBlocker, EffectiveConfigVerificationMismatch, @@ -45,6 +46,7 @@ import { clearKiloModelCacheSelection, inspectKiloModelCache, } from "./kilo-model-cache.js"; +import { fetchLiveInstallModelCatalog } from "./model-catalog.js"; export async function runInstallFlow( request: CliOptions, @@ -53,7 +55,9 @@ export async function runInstallFlow( let progressState: InstallProgressState = {}; const managedWrites = createManagedWriteTransaction(); let installFlow: PreparedInstallSession; + let installDependencies: InstallDependencies = dependencies; let previousInstallState: ManagedInstallStateRecord | undefined; + let resolvedSecret: ResolvedSecretInput; try { const resolvedContext = await resolveInstallContext(dependencies, { @@ -81,12 +85,26 @@ export async function runInstallFlow( dependencies, resolvedContext.workspace.managedPaths, ); + resolvedSecret = await resolveSecretInput( + { + apiKeyStdin: request.apiKeyStdin, + }, + dependencies, + ); + const liveModelCatalog = await fetchLiveInstallModelCatalog( + resolvedSecret.secret, + dependencies.http, + ); + installDependencies = { + ...dependencies, + models: liveModelCatalog, + }; const model = await resolveInstallModel( { modelKey: request.modelKey, yes: request.yes, }, - dependencies, + installDependencies, ); progressState = createInstallProgressState(resolvedContext.kilo, model); @@ -109,25 +127,25 @@ export async function runInstallFlow( try { await applyManagedWrites( - request, installFlow, previousInstallState, + resolvedSecret, managedWrites, - dependencies, + installDependencies, ); - await verifyPreparedInstall(installFlow, dependencies); - await persistInstallState(installFlow, managedWrites, dependencies); + await verifyPreparedInstall(installFlow, installDependencies); + await persistInstallState(installFlow, managedWrites, installDependencies); notices = await collectPostInstallNotices( request, installFlow, - dependencies, + installDependencies, ); } catch (error) { return await buildInstallFailureResult( error, progressState, managedWrites.rollbackActions, - dependencies, + installDependencies, ); } @@ -135,7 +153,7 @@ export async function runInstallFlow( installFlow, notices, progressState, - dependencies, + installDependencies, ); return ( @@ -144,19 +162,12 @@ export async function runInstallFlow( } async function applyManagedWrites( - request: CliOptions, installFlow: PreparedInstallSession, previousInstallState: ManagedInstallStateRecord | undefined, + resolvedSecret: ResolvedSecretInput, managedWrites: ReturnType, dependencies: InstallDependencies, ): Promise { - const resolvedSecret = await resolveSecretInput( - { - apiKeyStdin: request.apiKeyStdin, - }, - dependencies, - ); - await managedWrites.run( writeManagedSecret( { @@ -170,6 +181,7 @@ async function applyManagedWrites( writeScopeManagedConfigs( { managedPaths: installFlow.context.workspace.managedPaths, + modelCatalog: dependencies.models, model: installFlow.model.key, previousManagedModelKey: previousInstallState?.selectedModelKey, projectRoot: installFlow.context.workspace.projectRoot, diff --git a/src/install/managed-provider-config.ts b/src/install/managed-provider-config.ts index eb70012..c45c86d 100644 --- a/src/install/managed-provider-config.ts +++ b/src/install/managed-provider-config.ts @@ -4,35 +4,25 @@ import { GONKAGATE_PROVIDER_ID, GONKAGATE_PROVIDER_NAME, } from "../constants/gateway.js"; -import { - getCuratedModelByKey, - getValidatedModels, - type CuratedModel, - type CuratedModelCompatibility, - type CuratedModelKey, - type CuratedModelLimits, - type ValidatedCuratedModel, -} from "../constants/models.js"; import type { JsonObject } from "../json.js"; +import type { InstallModel, InstallModelCatalog } from "./model-catalog.js"; export const GONKAGATE_SECRET_FILE_REFERENCE = `{file:${GONKAGATE_MANAGED_SECRET_PATH}}`; -export function formatKiloModelRef( - model: CuratedModel | CuratedModelKey, -): string { +export function formatKiloModelRef(model: InstallModel | string): string { const key = typeof model === "string" ? model : model.key; return `${GONKAGATE_PROVIDER_ID}/${key}`; } export function buildManagedProviderConfig( - modelKey: CuratedModelKey, + modelKey: string, + catalog: InstallModelCatalog, ): JsonObject { - const model = resolveCuratedModel(modelKey); - const providerModels = createManagedProviderModels(model); + const model = resolveInstallModel(modelKey, catalog); const models: JsonObject = {}; - for (const providerModel of providerModels) { + for (const providerModel of createManagedProviderModels(model, catalog)) { models[providerModel.key] = createManagedModelConfig(providerModel); } @@ -40,90 +30,58 @@ export function buildManagedProviderConfig( models, name: GONKAGATE_PROVIDER_NAME, npm: model.adapterPackage, - options: createManagedProviderOptions(model), + options: createManagedProviderOptions(), }; } function createManagedProviderModels( - selectedModel: CuratedModel, -): readonly ValidatedCuratedModel[] { - const providerModels = getValidatedModels().filter( - (model) => - model.transport === selectedModel.transport && - model.adapterPackage === selectedModel.adapterPackage, - ); + selectedModel: InstallModel, + catalog: InstallModelCatalog, +): readonly InstallModel[] { + const providerModels = catalog + .getModels() + .filter( + (model) => + model.transport === selectedModel.transport && + model.adapterPackage === selectedModel.adapterPackage, + ); if (providerModels.some((model) => model.key === selectedModel.key)) { return providerModels; } throw new Error( - `Validated model catalog does not include selected model: ${selectedModel.key}`, + `Live model catalog does not include selected model: ${selectedModel.key}`, ); } -function createManagedModelConfig(model: ValidatedCuratedModel): JsonObject { - const runtimeCompatibility = model.runtimeCompatibility as - | CuratedModelCompatibility - | undefined; - const limits = model.limits as CuratedModelLimits | undefined; - const modelConfig: JsonObject = { +function createManagedModelConfig(model: InstallModel): JsonObject { + return { id: model.modelId, + limit: { + context: model.limits.context, + output: model.limits.output, + }, name: model.displayName, tool_call: true, }; - - if ( - typeof limits?.context === "number" || - typeof limits?.output === "number" - ) { - const limit: JsonObject = {}; - - if (typeof limits?.context === "number") { - limit.context = limits.context; - } - - if (typeof limits?.output === "number") { - limit.output = limits.output; - } - - modelConfig.limit = limit; - } - - const modelOptions = runtimeCompatibility?.modelOptions; - - if (modelOptions !== undefined) { - Object.assign(modelConfig, modelOptions); - } - - return modelConfig; } -function createManagedProviderOptions(model: CuratedModel): JsonObject { - const runtimeCompatibility = model.runtimeCompatibility as - | CuratedModelCompatibility - | undefined; - const providerOptions: JsonObject = { +function createManagedProviderOptions(): JsonObject { + return { apiKey: GONKAGATE_SECRET_FILE_REFERENCE, baseURL: GONKAGATE_BASE_URL, }; - - if (runtimeCompatibility?.providerOptions !== undefined) { - Object.assign(providerOptions, runtimeCompatibility.providerOptions); - } - - return providerOptions; } -function resolveCuratedModel(modelKey: CuratedModelKey): CuratedModel { - const model = getCuratedModelByKey(modelKey); +function resolveInstallModel( + modelKey: string, + catalog: InstallModelCatalog, +): InstallModel { + const model = catalog.getModelByKey(modelKey); if (model === undefined) { - throw new Error(`Unsupported curated model key: ${modelKey}`); - } - - if (model.validationStatus !== "validated") { - throw new Error(`Unvalidated curated model key: ${modelKey}`); + throw new Error(`Unsupported GonkaGate model id: ${modelKey}`); } return model; diff --git a/src/install/model-catalog.ts b/src/install/model-catalog.ts index f975839..d99be8f 100644 --- a/src/install/model-catalog.ts +++ b/src/install/model-catalog.ts @@ -1,26 +1,199 @@ -import { - getCuratedModelByKey, - getRecommendedProductionDefaultModel, - getValidatedModels, - type CuratedModel, - type RecommendedProductionDefaultCuratedModel, - type ValidatedCuratedModel, -} from "../constants/models.js"; +import { CURRENT_TRANSPORT, GONKAGATE_BASE_URL } from "../constants/gateway.js"; +import { createInstallError } from "./errors.js"; + +export type InstallModelTransport = typeof CURRENT_TRANSPORT; +export type InstallModelKey = string; + +export interface InstallModelLimits { + context: number; + output: number; +} + +export interface InstallModel { + adapterPackage: string; + displayName: string; + key: InstallModelKey; + limits: InstallModelLimits; + modelId: string; + recommended: boolean; + transport: InstallModelTransport; + validationStatus: "validated"; +} export interface InstallModelCatalog { - getCuratedModelByKey(key: string): CuratedModel | undefined; - getRecommendedProductionDefaultModel(): - | RecommendedProductionDefaultCuratedModel - | undefined; - getValidatedModels(): readonly ValidatedCuratedModel[]; + getModelByKey(key: string): InstallModel | undefined; + getModels(): readonly InstallModel[]; + getRecommendedDefaultModel(): InstallModel | undefined; +} + +export interface InstallHttpJsonResponse { + body: unknown; + ok: boolean; + status: number; +} + +export interface InstallHttpClient { + fetchJson( + url: string, + options?: { + headers?: Readonly>; + method?: "GET"; + }, + ): Promise; +} + +const GONKAGATE_MODELS_URL = `${GONKAGATE_BASE_URL}/models`; +const OPENAI_COMPATIBLE_ADAPTER_PACKAGE = "@ai-sdk/openai-compatible"; +const GENERIC_KILO_CONTEXT_LIMIT = 240000; +const GENERIC_KILO_OUTPUT_LIMIT = 8192; + +export async function fetchLiveInstallModelCatalog( + apiKey: string, + http: InstallHttpClient, +): Promise { + let response: InstallHttpJsonResponse; + + try { + response = await http.fetchJson(GONKAGATE_MODELS_URL, { + headers: { + Accept: "application/json", + Authorization: `Bearer ${apiKey}`, + }, + method: "GET", + }); + } catch { + throw createInstallError("validated_models_unavailable", {}); + } + + if (!response.ok) { + throw createInstallError("validated_models_unavailable", {}); + } + + return createInstallModelCatalog(parseLiveModelsResponse(response.body)); } -const DEFAULT_INSTALL_MODEL_CATALOG: InstallModelCatalog = { - getCuratedModelByKey, - getRecommendedProductionDefaultModel, - getValidatedModels, -}; +export function createInstallModelCatalog( + models: readonly InstallModel[], +): InstallModelCatalog { + const modelsByKey = new Map(models.map((model) => [model.key, model])); + const recommendedModel = models.find((model) => model.recommended); + + return { + getModelByKey(key) { + return modelsByKey.get(key); + }, + getModels() { + return models; + }, + getRecommendedDefaultModel() { + return recommendedModel; + }, + }; +} export function createDefaultInstallModelCatalog(): InstallModelCatalog { - return DEFAULT_INSTALL_MODEL_CATALOG; + return createInstallModelCatalog([]); +} + +function parseLiveModelsResponse(value: unknown): readonly InstallModel[] { + if (!isObjectRecord(value) || !Array.isArray(value.data)) { + throw createInstallError("validated_models_unavailable", {}); + } + + const seenIds = new Set(); + const models: InstallModel[] = []; + let responseDefaultId: string | undefined; + + for (const entry of value.data) { + const parsedModel = parseLiveModel(entry); + + if (seenIds.has(parsedModel.key)) { + continue; + } + + seenIds.add(parsedModel.key); + models.push(parsedModel); + + if (responseDefaultId === undefined && isResponseDefaultModel(entry)) { + responseDefaultId = parsedModel.key; + } + } + + if (models.length === 0) { + throw createInstallError("validated_models_unavailable", {}); + } + + const defaultModelId = responseDefaultId ?? models[0]?.key; + + return Object.freeze( + models.map((model) => ({ + ...model, + recommended: model.key === defaultModelId, + })), + ); +} + +function parseLiveModel(value: unknown): InstallModel { + if (!isObjectRecord(value)) { + throw createInstallError("validated_models_unavailable", {}); + } + + const id = parseRequiredNonEmptyString(value.id); + const name = parseOptionalNonEmptyString(value.name) ?? id; + + return { + adapterPackage: OPENAI_COMPATIBLE_ADAPTER_PACKAGE, + displayName: name, + key: id, + limits: { + context: GENERIC_KILO_CONTEXT_LIMIT, + output: GENERIC_KILO_OUTPUT_LIMIT, + }, + modelId: id, + recommended: false, + transport: CURRENT_TRANSPORT, + validationStatus: "validated", + }; +} + +function parseRequiredNonEmptyString(value: unknown): string { + if (typeof value !== "string") { + throw createInstallError("validated_models_unavailable", {}); + } + + const trimmedValue = value.trim(); + + if (trimmedValue.length === 0) { + throw createInstallError("validated_models_unavailable", {}); + } + + return trimmedValue; +} + +function parseOptionalNonEmptyString(value: unknown): string | undefined { + if (value === undefined) { + return undefined; + } + + if (typeof value !== "string") { + throw createInstallError("validated_models_unavailable", {}); + } + + const trimmedValue = value.trim(); + + return trimmedValue.length === 0 ? undefined : trimmedValue; +} + +function isResponseDefaultModel(value: unknown): boolean { + return isObjectRecord(value) && value.default === true; +} + +export function isInstallModelTransport( + value: unknown, +): value is InstallModelTransport { + return value === CURRENT_TRANSPORT; +} + +function isObjectRecord(value: unknown): value is Record { + return typeof value === "object" && value !== null && !Array.isArray(value); } diff --git a/src/install/scope.ts b/src/install/scope.ts index 2f36bcb..ab2589e 100644 --- a/src/install/scope.ts +++ b/src/install/scope.ts @@ -1,4 +1,3 @@ -import { type CuratedModelKey } from "../constants/models.js"; import { MANAGED_CONFIG_PLANS, type ManagedConfigTarget, @@ -21,6 +20,7 @@ import { buildManagedProviderConfig, formatKiloModelRef, } from "./managed-provider-config.js"; +import type { InstallModelKey } from "./model-catalog.js"; import { resolveProjectConfigTargetPath, resolveUserConfigTargetPath, @@ -51,7 +51,10 @@ function createManagedConfigWriteContext( managedPaths: request.managedPaths, ownedActivationModelRefs: createOwnedActivationModelRefs(request), projectRoot: request.projectRoot, - providerConfig: buildManagedProviderConfig(request.model), + providerConfig: buildManagedProviderConfig( + request.model, + request.modelCatalog, + ), }; } @@ -178,8 +181,8 @@ function requireTargetWriteResult( } export function createOwnedActivationModelRefsForTesting( - model: CuratedModelKey, - previousManagedModelKey?: CuratedModelKey, + model: InstallModelKey, + previousManagedModelKey?: InstallModelKey, ): readonly string[] { return createOwnedActivationModelRefs({ model, diff --git a/src/install/selection.ts b/src/install/selection.ts index 2f33775..7a96754 100644 --- a/src/install/selection.ts +++ b/src/install/selection.ts @@ -1,11 +1,8 @@ -import type { - CuratedModelKey, - ValidatedCuratedModel, -} from "../constants/models.js"; import { formatKiloModelRef } from "./managed-provider-config.js"; import type { InstallDependencies, InstallSelectChoice } from "./deps.js"; import { createInstallError } from "./errors.js"; import type { InstallScope } from "./contracts.js"; +import type { InstallModel, InstallModelKey } from "./model-catalog.js"; export interface ModelSelectionRequest { modelKey?: string; @@ -34,10 +31,10 @@ export function getRecommendedInstallScope( export async function resolveInstallModel( request: ModelSelectionRequest, dependencies: InstallDependencies, -): Promise { - const validatedModels = dependencies.models.getValidatedModels(); +): Promise { + const models = dependencies.models.getModels(); - if (validatedModels.length === 0) { + if (models.length === 0) { throw createInstallError("validated_models_unavailable", {}); } @@ -45,44 +42,21 @@ export async function resolveInstallModel( return requireValidatedModel(request.modelKey, dependencies); } - const recommendedModel = - dependencies.models.getRecommendedProductionDefaultModel(); - const singleValidatedModel = - validatedModels.length === 1 ? validatedModels[0] : undefined; - const defaultPromptModel = - recommendedModel ?? singleValidatedModel ?? validatedModels[0]; + const defaultModel = dependencies.models.getRecommendedDefaultModel(); if (request.yes) { - if (recommendedModel !== undefined) { - return recommendedModel; - } - - if (singleValidatedModel !== undefined) { - return singleValidatedModel; - } + return defaultModel ?? models[0]!; } if (!canUseInteractiveInstallPrompts(dependencies)) { - if (recommendedModel !== undefined) { - return recommendedModel; - } - - if (singleValidatedModel !== undefined) { - return singleValidatedModel; - } - - throw createInstallError("model_selection_required", { - validatedModelCount: validatedModels.length, - }); + return defaultModel ?? models[0]!; } const selectedModelKey = await dependencies.prompts.selectOption({ - choices: validatedModels.map((model) => - createModelChoice(model, defaultPromptModel), - ), - defaultValue: defaultPromptModel.key, + choices: models.map((model) => createModelChoice(model, defaultModel)), + defaultValue: (defaultModel ?? models[0]!).key, message: "Choose the GonkaGate model to configure for Kilo", - pageSize: Math.min(8, validatedModels.length), + pageSize: Math.min(8, models.length), }); return requireValidatedModel(selectedModelKey, dependencies); @@ -131,13 +105,13 @@ export async function resolveInstallScope( } function createModelChoice( - model: ValidatedCuratedModel, - defaultModel: ValidatedCuratedModel, -): InstallSelectChoice { + model: InstallModel, + defaultModel: InstallModel | undefined, +): InstallSelectChoice { return { - description: `${formatKiloModelRef(model)} ยท validated`, + description: formatKiloModelRef(model), label: - model.key === defaultModel.key + model.key === defaultModel?.key ? `${model.displayName} (Recommended)` : model.displayName, value: model.key, @@ -185,14 +159,14 @@ function formatScopeLabel(scope: InstallScope): string { function requireValidatedModel( modelKey: string, dependencies: InstallDependencies, -): ValidatedCuratedModel { - const model = dependencies.models.getCuratedModelByKey(modelKey); +): InstallModel { + const model = dependencies.models.getModelByKey(modelKey); - if (model === undefined || model.validationStatus !== "validated") { + if (model === undefined) { throw createInstallError("unsupported_model_key", { modelKey, }); } - return model as ValidatedCuratedModel; + return model; } diff --git a/src/install/session.ts b/src/install/session.ts index a8b8a15..146cf46 100644 --- a/src/install/session.ts +++ b/src/install/session.ts @@ -13,12 +13,9 @@ import { KILO_PREFERRED_PROJECT_CONFIG, KILO_PRIMARY_COMMAND, } from "./kilo.js"; -import type { - CuratedModelKey, - ValidatedCuratedModel, -} from "../constants/models.js"; import type { ResolvedInstallContext } from "./context.js"; import { formatKiloModelRef } from "./managed-provider-config.js"; +import type { InstallModel } from "./model-catalog.js"; import type { InstalledInstallResult, InstallFlowProgress, @@ -32,30 +29,27 @@ export type InstallProgressState = InstallFlowProgress; export interface PreparedInstallSession { context: ResolvedInstallContext; - model: ValidatedCuratedModel; + model: InstallModel; summary: InstallFlowSelectionSummary; } export function createInstallProgressState( kilo: SupportedKiloSummary, - model?: ValidatedCuratedModel, + model?: InstallModel, scope?: InstallScope, ): InstallProgressState { return { kilo, modelDisplayName: model?.displayName, modelKey: model?.key, - modelRef: - model === undefined - ? undefined - : (formatKiloModelRef(model) as `gonkagate/${CuratedModelKey}`), + modelRef: model === undefined ? undefined : formatKiloModelRef(model), scope, }; } export function prepareInstallSession( context: ResolvedInstallContext, - model: ValidatedCuratedModel, + model: InstallModel, scope: InstallScope, ): PreparedInstallSession { return { diff --git a/src/install/state.ts b/src/install/state.ts index 7c3beff..d47c036 100644 --- a/src/install/state.ts +++ b/src/install/state.ts @@ -1,12 +1,9 @@ import { CONTRACT_METADATA } from "../constants/contract.js"; -import { - isCuratedModelKey, - isCuratedModelTransport, -} from "../constants/models.js"; import type { ManagedInstallStateRecord } from "./contracts/install-state.js"; import type { ManagedArtifactWriteResult } from "./contracts/managed-artifact.js"; import type { InstallDependencies } from "./deps.js"; import { createInstallError } from "./errors.js"; +import { isInstallModelTransport } from "./model-catalog.js"; import { assertUserProfileScopedManagedPath, replaceManagedTextFile, @@ -124,12 +121,12 @@ function parseManagedInstallStateRecord( const selectedModelKey = getRequiredString(value, "selectedModelKey"); const selectedScope = getRequiredString(value, "selectedScope"); - if (!isCuratedModelTransport(currentTransport)) { + if (!isInstallModelTransport(currentTransport)) { throw new Error("Managed install state has an unknown currentTransport."); } - if (!isCuratedModelKey(selectedModelKey)) { - throw new Error("Managed install state has an unknown selectedModelKey."); + if (selectedModelKey.trim().length === 0) { + throw new Error("Managed install state has an empty selectedModelKey."); } if (selectedScope !== "project" && selectedScope !== "user") { diff --git a/src/install/verification-blockers.ts b/src/install/verification-blockers.ts index 6e92dba..b27a234 100644 --- a/src/install/verification-blockers.ts +++ b/src/install/verification-blockers.ts @@ -212,7 +212,7 @@ export function collectProviderShapeBlockers( ); } -export function collectMissingCuratedModelEntryBlockers( +export function collectMissingSelectedModelEntryBlockers( config: Record, layer: EffectiveConfigVerificationBlockingLayer, options: { @@ -253,7 +253,7 @@ export function collectMissingCuratedModelEntryBlockers( layer, path, reason: - "Higher-precedence config defines the GonkaGate provider without the curated model entry.", + "Higher-precedence config defines the GonkaGate provider without the selected model entry.", }, ]; } diff --git a/src/install/verification-notes.ts b/src/install/verification-notes.ts index 318ac2c..b912539 100644 --- a/src/install/verification-notes.ts +++ b/src/install/verification-notes.ts @@ -9,7 +9,7 @@ export const KILO_VERIFICATION_NOTES = Object.freeze([ `${KILO_DEBUG_CONFIG_COMMAND} exists in the investigated Kilo baseline, but raw output must be treated as secret-bearing.`, `${KILO_DEBUG_CONFIG_COMMAND} --pure is not documented in the investigated Kilo baseline.`, `${KILO_CONFIG_ENV_VAR} and ${KILO_CONFIG_CONTENT_ENV_VAR} are Kilo override layers, not durable installer targets.`, - "The curated Kimi K2.6 default writes limit.output = 8192 because Kilo 7.2.0 requires a numeric output limit in custom model config.", + "Live GonkaGate model entries write limit.output = 8192 because Kilo 7.2.0 requires a numeric output limit in custom model config.", "The installer accepts @kilocode/cli >=7.2.0 without pre-blocking future Kilo releases; the audited compatibility baseline remains 7.2.0.", ] as const); diff --git a/src/install/verify-effective.ts b/src/install/verify-effective.ts index d0cc3ed..eee1aca 100644 --- a/src/install/verify-effective.ts +++ b/src/install/verify-effective.ts @@ -1,4 +1,3 @@ -import type { CuratedModelKey } from "../constants/models.js"; import type { EffectiveConfigVerificationBlocker, EffectiveConfigVerificationMismatch, @@ -8,6 +7,7 @@ import type { InstallScope, KiloCommandName } from "./contracts.js"; import type { InstallDependencies } from "./deps.js"; import { createResolvedConfigVerificationPolicy } from "./effective-config-policy.js"; import { createInstallError } from "./errors.js"; +import type { InstallModelKey } from "./model-catalog.js"; import { tryParseJsoncObject } from "./jsonc.js"; import { runKiloOracle } from "./kilo-oracle.js"; import type { ManagedPaths } from "./paths.js"; @@ -24,7 +24,7 @@ import { collectValueCheckMismatches } from "./verification-mismatches.js"; export interface EffectiveKiloConfigVerificationRequest { kiloCommand: KiloCommandName; managedPaths: ManagedPaths; - model: CuratedModelKey; + model: InstallModelKey; projectRoot: string; scope: InstallScope; } @@ -36,6 +36,7 @@ export async function verifyEffectiveKiloConfig( const verificationRequest = toLayerInspectionRequest(request); const verificationPolicy = createResolvedConfigVerificationPolicy( request.model, + dependencies.models, ); const durableResolution = await resolveDurableLocalKiloConfig( verificationRequest, @@ -108,6 +109,7 @@ export async function verifyCurrentSessionKiloConfig( }); const verificationPolicy = createResolvedConfigVerificationPolicy( request.model, + dependencies.models, ); const layerBlockers = selectHighestPrecedenceInspectableBlockers([ ...(await inspectVerificationLayers(verificationRequest, dependencies, { diff --git a/src/install/verify-layers.ts b/src/install/verify-layers.ts index 7747ce6..16fc0cc 100644 --- a/src/install/verify-layers.ts +++ b/src/install/verify-layers.ts @@ -6,7 +6,6 @@ import type { EffectiveConfigVerificationLayerSnapshot, } from "./contracts/effective-config.js"; import type { InstallScope } from "./contracts.js"; -import type { CuratedModelKey } from "../constants/models.js"; import type { InstallDependencies } from "./deps.js"; import { createInstallError } from "./errors.js"; import { tryParseJsoncObject } from "./jsonc.js"; @@ -23,7 +22,7 @@ import type { ManagedPaths } from "./paths.js"; import { resolveInspectableSystemManagedConfigPaths } from "./paths.js"; import { collectManagedOverlapBlockers, - collectMissingCuratedModelEntryBlockers, + collectMissingSelectedModelEntryBlockers, collectProviderActivationBlockers, collectProviderModelFilterBlockers, collectProviderShapeBlockers, @@ -36,6 +35,7 @@ import { isPathInside, normalizeInstallPath, } from "./platform-path.js"; +import type { InstallModelKey } from "./model-catalog.js"; const INSPECTABLE_LAYER_PRECEDENCE = Object.freeze({ global_config: 0, @@ -51,7 +51,7 @@ const INSPECTABLE_LAYER_PRECEDENCE = Object.freeze({ export interface VerificationLayerInspectionRequest { managedPaths: ManagedPaths; - model: CuratedModelKey; + model: InstallModelKey; projectRoot: string; providerId: string; scope: InstallScope; @@ -134,7 +134,7 @@ export async function inspectVerificationLayers( request.providerId, layerPath, ), - ...collectMissingCuratedModelEntryBlockers( + ...collectMissingSelectedModelEntryBlockers( layerSnapshot.config, layerSnapshot.source.layer, { diff --git a/test/cli.test.ts b/test/cli.test.ts index 840018b..4e57ae2 100644 --- a/test/cli.test.ts +++ b/test/cli.test.ts @@ -3,16 +3,10 @@ import { spawnSync } from "node:child_process"; import test from "node:test"; import { resolve } from "node:path"; import { pathToFileURL } from "node:url"; -import type { - CuratedModelKey, - RecommendedProductionDefaultCuratedModel, - ValidatedCuratedModel, -} from "../src/constants/models.js"; import { CONTRACT_METADATA } from "../src/constants/contract.js"; import { GONKAGATE_BASE_URL } from "../src/constants/gateway.js"; import { parseCliOptions, renderCliEntrypointError, run } from "../src/cli.js"; import type { InstallSelectOptions } from "../src/install/deps.js"; -import type { InstallModelCatalog } from "../src/install/model-catalog.js"; import { buildManagedProviderConfig, formatKiloModelRef, @@ -20,21 +14,12 @@ import { import { createStubbedTestInstallDependencies } from "./install/test-deps.js"; import type { TestInstallFsFileSeed } from "./install/test-deps.js"; import { escapeRegExp, repoRoot } from "./contract-helpers.js"; +import { + createValidatedTestModelCatalog, + TEST_VALIDATED_MODEL, +} from "./install/test-model-catalog.js"; -const MODEL_KEY = "kimi-k2.6" as const; -const VALIDATED_MODEL: RecommendedProductionDefaultCuratedModel = { - adapterPackage: "@ai-sdk/openai-compatible", - displayName: "Kimi K2.6", - key: MODEL_KEY, - limits: { - context: 240000, - output: 8192, - }, - modelId: "moonshotai/Kimi-K2.6", - recommended: true, - transport: "chat_completions", - validationStatus: "validated", -}; +const MODEL_KEY = TEST_VALIDATED_MODEL.key; type TestSelectOption = ( options: InstallSelectOptions, @@ -54,27 +39,14 @@ function createBufferWriter(): BufferWriter { }; } -function createValidatedModelCatalog(): InstallModelCatalog { - return { - getCuratedModelByKey(key) { - return key === MODEL_KEY ? VALIDATED_MODEL : undefined; - }, - getRecommendedProductionDefaultModel() { - return VALIDATED_MODEL; - }, - getValidatedModels() { - return [VALIDATED_MODEL]; - }, - }; -} - function createResolvedConfigFixture( mutate?: (config: Record) => void, ): string { + const catalog = createValidatedTestModelCatalog(); const resolvedConfig = { model: formatKiloModelRef(MODEL_KEY), provider: { - gonkagate: buildManagedProviderConfig(MODEL_KEY), + gonkagate: buildManagedProviderConfig(MODEL_KEY, catalog), }, } satisfies Record; const nextConfig = structuredClone(resolvedConfig); @@ -118,7 +90,7 @@ function createCliDependencies( }, models: { kind: "override", - value: createValidatedModelCatalog(), + value: createValidatedTestModelCatalog(), }, prompts: options.selectOption === undefined @@ -196,7 +168,7 @@ test("CLI wrapper exposes the runtime help surface", () => { assert.equal(helpResult.status, 0); assert.match(helpResult.stdout, /Usage: kilo-setup/i); assert.match(helpResult.stdout, /Configure Kilo to use GonkaGate/i); - assert.match(helpResult.stdout, /validated Kimi K2\.6/i); + assert.match(helpResult.stdout, /fetches GonkaGate models/i); assert.match(helpResult.stdout, /--scope /); assert.match(helpResult.stdout, /--api-key-stdin/); assert.match(helpResult.stdout, /GONKAGATE_API_KEY/); @@ -240,7 +212,7 @@ test("interactive runs show the public model picker even when one validated mode promptMessages[0] ?? "", /Choose the GonkaGate model to configure for Kilo/i, ); - assert.deepEqual(promptChoices[0], ["Kimi K2.6 (Recommended)"]); + assert.deepEqual(promptChoices[0], ["Live Test Model (Recommended)"]); assert.equal(promptMessages.length, 1); }); @@ -394,7 +366,7 @@ test("CLI omits undefined blocked kilo details from JSON output", async () => { dependencies: createStubbedTestInstallDependencies({ models: { kind: "override", - value: createValidatedModelCatalog(), + value: createValidatedTestModelCatalog(), }, runtime: { cwd: "/workspace/project", diff --git a/test/docs-contract.test.ts b/test/docs-contract.test.ts index a43f4cd..1ff81c0 100644 --- a/test/docs-contract.test.ts +++ b/test/docs-contract.test.ts @@ -40,7 +40,7 @@ test("docs describe the shipped Kilo runtime and the remaining public proof gate /\.kilo\/kilo\.jsonc/, new RegExp(escapeRegExp(GONKAGATE_BASE_URL)), /chat\/completions|chat_completions/, - /runtime is shipped|validated default|Kimi K2\.6|blocks unsupported kilo|rollback/i, + /runtime is shipped|GET \/v1\/models|blocks unsupported kilo|rollback/i, /no plain `--api-key`|never accept plain `--api-key`|never accept `--api-key`/i, /provider\.gonkagate/, /each participating machine|each machine/i, diff --git a/test/install/flow.test.ts b/test/install/flow.test.ts index e999d94..7a6b952 100644 --- a/test/install/flow.test.ts +++ b/test/install/flow.test.ts @@ -20,10 +20,11 @@ import { function createResolvedConfigFixture( mutate?: (config: Record) => void, ): string { + const catalog = createValidatedTestModelCatalog(); const resolvedConfig = { model: formatKiloModelRef(TEST_VALIDATED_MODEL.key), provider: { - gonkagate: buildManagedProviderConfig(TEST_VALIDATED_MODEL.key), + gonkagate: buildManagedProviderConfig(TEST_VALIDATED_MODEL.key, catalog), }, } satisfies Record; const nextConfig = structuredClone(resolvedConfig); @@ -72,6 +73,13 @@ function createFlowDependencies( ? createEmptyTestModelCatalog() : createValidatedTestModelCatalog(), }, + http: + options.models === "empty" + ? { + body: { data: [] }, + kind: "stub", + } + : undefined, prompts: { kind: "stub", secret: "gp-flow-secret", diff --git a/test/install/models.test.ts b/test/install/models.test.ts index fdbbab4..2e11385 100644 --- a/test/install/models.test.ts +++ b/test/install/models.test.ts @@ -1,88 +1,99 @@ import assert from "node:assert/strict"; import test from "node:test"; +import { GONKAGATE_BASE_URL } from "../../src/constants/gateway.js"; import { - CURATED_MODEL_REGISTRY, - createCuratedModelIndex, - getRecommendedProductionDefaultModel, - getValidatedModelKeys, -} from "../../src/constants/models.js"; + fetchLiveInstallModelCatalog, + type InstallHttpClient, +} from "../../src/install/model-catalog.js"; +import { expectInstallErrorCode } from "./test-helpers.js"; -test("the default curated model is the shipped validated Kimi public default", () => { - const model = CURATED_MODEL_REGISTRY["kimi-k2.6"]; +test("fetchLiveInstallModelCatalog uses /v1/models as the source of truth", async () => { + let requestedUrl = ""; + let requestedAuthorization = ""; + const catalog = await fetchLiveInstallModelCatalog("gp-test-secret", { + async fetchJson(url, options) { + requestedUrl = url; + requestedAuthorization = options?.headers?.Authorization ?? ""; - assert.equal(model.adapterPackage, "@ai-sdk/openai-compatible"); - assert.equal(model.validationStatus, "validated"); - assert.equal(model.limits?.context, 240000); - assert.equal(model.limits?.output, 8192); - assert.deepEqual(getValidatedModelKeys(), [ - "kimi-k2.6", - "qwen3-235b-a22b-instruct-2507-fp8", - "minimax-m2.7", - ]); - assert.equal(getRecommendedProductionDefaultModel()?.key, "kimi-k2.6"); + return { + body: { + data: [ + { id: "provider/live-alpha", name: "Live Alpha" }, + { id: "provider/live-beta" }, + { id: "provider/live-alpha", name: "Ignored Duplicate" }, + ], + }, + ok: true, + status: 200, + }; + }, + }); + + assert.equal(requestedUrl, `${GONKAGATE_BASE_URL}/models`); + assert.equal(requestedAuthorization, "Bearer gp-test-secret"); + assert.deepEqual( + catalog.getModels().map((model) => model.key), + ["provider/live-alpha", "provider/live-beta"], + ); + assert.equal( + catalog.getModelByKey("provider/live-alpha")?.displayName, + "Live Alpha", + ); + assert.equal( + catalog.getModelByKey("provider/live-beta")?.displayName, + "provider/live-beta", + ); + assert.equal( + catalog.getRecommendedDefaultModel()?.key, + "provider/live-alpha", + ); }); -test("createCuratedModelIndex exposes a recommended production default when a validated model is recommended", () => { - const testRegistry = { - alpha: { - adapterPackage: "@ai-sdk/openai-compatible", - displayName: "Alpha", - limits: { - context: 1024, - }, - modelId: "gonkagate/alpha", - recommended: false, - transport: "chat_completions", - validationStatus: "validated", - }, - beta: { - adapterPackage: "@ai-sdk/openai-compatible", - displayName: "Beta", - limits: { - context: 1024, - output: 2048, - }, - modelId: "gonkagate/beta", - recommended: true, - transport: "chat_completions", - validationStatus: "validated", +test("fetchLiveInstallModelCatalog honors an API-provided default model", async () => { + const catalog = await fetchLiveInstallModelCatalog("gp-test-secret", { + async fetchJson() { + return { + body: { + data: [ + { id: "provider/live-alpha", name: "Live Alpha" }, + { default: true, id: "provider/live-beta", name: "Live Beta" }, + ], + }, + ok: true, + status: 200, + }; }, - } as const; + }); - const index = createCuratedModelIndex(testRegistry); - - assert.deepEqual(index.validatedModelKeys, ["alpha", "beta"]); - assert.equal(index.recommendedProductionDefaultModel?.key, "beta"); + assert.equal(catalog.getRecommendedDefaultModel()?.key, "provider/live-beta"); }); -test("createCuratedModelIndex rejects more than one recommended validated production default", () => { - const invalidRegistry = { - alpha: { - adapterPackage: "@ai-sdk/openai-compatible", - displayName: "Alpha", - limits: { - output: 512, - }, - modelId: "gonkagate/alpha", - recommended: true, - transport: "chat_completions", - validationStatus: "validated", +test("fetchLiveInstallModelCatalog rejects empty or invalid responses", async () => { + const emptyHttp: InstallHttpClient = { + async fetchJson() { + return { + body: { data: [] }, + ok: true, + status: 200, + }; }, - beta: { - adapterPackage: "@ai-sdk/openai-compatible", - displayName: "Beta", - limits: { - output: 256, - }, - modelId: "gonkagate/beta", - recommended: true, - transport: "chat_completions", - validationStatus: "validated", + }; + const invalidHttp: InstallHttpClient = { + async fetchJson() { + return { + body: { data: [{ name: "Missing id" }] }, + ok: true, + status: 200, + }; }, - } as const; + }; - assert.throws( - () => createCuratedModelIndex(invalidRegistry), - /recommended validated production default/i, + await assert.rejects( + () => fetchLiveInstallModelCatalog("gp-test-secret", emptyHttp), + expectInstallErrorCode("validated_models_unavailable"), + ); + await assert.rejects( + () => fetchLiveInstallModelCatalog("gp-test-secret", invalidHttp), + expectInstallErrorCode("validated_models_unavailable"), ); }); diff --git a/test/install/scope.test.ts b/test/install/scope.test.ts index 91c4894..a2ec974 100644 --- a/test/install/scope.test.ts +++ b/test/install/scope.test.ts @@ -4,6 +4,11 @@ import { writeScopeManagedConfigs } from "../../src/install/scope.js"; import { resolveManagedPaths } from "../../src/install/paths.js"; import type { StubInstallFs } from "./test-deps.js"; import { createStubbedTestInstallDependencies } from "./test-deps.js"; +import { + createValidatedTestModelCatalog, + TEST_EXTRA_MODEL, + TEST_VALIDATED_MODEL, +} from "./test-model-catalog.js"; function createScopeTestContext( options: { @@ -13,6 +18,10 @@ function createScopeTestContext( const dependencies = createStubbedTestInstallDependencies({ seedFiles: options.seedFiles, }); + const modelCatalog = createValidatedTestModelCatalog([ + TEST_VALIDATED_MODEL, + TEST_EXTRA_MODEL, + ]); const projectRoot = dependencies.runtime.cwd; const managedPaths = resolveManagedPaths( dependencies.runtime.homeDir, @@ -24,6 +33,7 @@ function createScopeTestContext( dependencies, fs: dependencies.fs as StubInstallFs, managedPaths, + modelCatalog, projectRoot, }; } @@ -32,8 +42,7 @@ test("user scope writes provider and activation to user config and removes owned const context = createScopeTestContext({ seedFiles: [ { - contents: - '{\n "model": "gonkagate/qwen3-235b-a22b-instruct-2507-fp8",\n "small_model": "custom/small"\n}\n', + contents: `{\n "model": "${formatModelRef(TEST_VALIDATED_MODEL.key)}",\n "small_model": "custom/small"\n}\n`, path: "/workspace/project/.kilo/kilo.jsonc", }, ], @@ -42,7 +51,8 @@ test("user scope writes provider and activation to user config and removes owned const result = await writeScopeManagedConfigs( { managedPaths: context.managedPaths, - model: "qwen3-235b-a22b-instruct-2507-fp8", + model: TEST_VALIDATED_MODEL.key, + modelCatalog: context.modelCatalog, projectRoot: context.projectRoot, scope: "user", }, @@ -66,10 +76,15 @@ test("user scope writes provider and activation to user config and removes owned ); assert.match( userConfigText, - /"model": "gonkagate\/qwen3-235b-a22b-instruct-2507-fp8"/, + new RegExp( + `"model": "${escapeRegExp(formatModelRef(TEST_VALIDATED_MODEL.key))}"`, + ), ); - assert.match(userConfigText, /"kimi-k2\.6"/); - assert.match(userConfigText, /"qwen3-235b-a22b-instruct-2507-fp8"/); + assert.match( + userConfigText, + new RegExp(escapeRegExp(TEST_VALIDATED_MODEL.key)), + ); + assert.match(userConfigText, new RegExp(escapeRegExp(TEST_EXTRA_MODEL.key))); assert.doesNotMatch(userConfigText, /small_model/); assert.doesNotMatch(projectConfigText, /"model": "gonkagate\//); assert.match(projectConfigText, /"small_model": "custom\/small"/); @@ -81,7 +96,8 @@ test("project scope writes provider only to user config and activation only to p const result = await writeScopeManagedConfigs( { managedPaths: context.managedPaths, - model: "qwen3-235b-a22b-instruct-2507-fp8", + model: TEST_VALIDATED_MODEL.key, + modelCatalog: context.modelCatalog, projectRoot: context.projectRoot, scope: "project", }, @@ -109,7 +125,8 @@ test("scope writes are idempotent on rerun", async () => { const firstWrite = await writeScopeManagedConfigs( { managedPaths: context.managedPaths, - model: "qwen3-235b-a22b-instruct-2507-fp8", + model: TEST_VALIDATED_MODEL.key, + modelCatalog: context.modelCatalog, projectRoot: context.projectRoot, scope: "project", }, @@ -118,7 +135,8 @@ test("scope writes are idempotent on rerun", async () => { const secondWrite = await writeScopeManagedConfigs( { managedPaths: context.managedPaths, - model: "qwen3-235b-a22b-instruct-2507-fp8", + model: TEST_VALIDATED_MODEL.key, + modelCatalog: context.modelCatalog, projectRoot: context.projectRoot, scope: "project", }, @@ -134,8 +152,7 @@ test("scope switch removes only installer-owned stale activation from the old ta const context = createScopeTestContext({ seedFiles: [ { - contents: - '{\n "model": "gonkagate/qwen3-235b-a22b-instruct-2507-fp8",\n "small_model": "custom/small"\n}\n', + contents: `{\n "model": "${formatModelRef(TEST_VALIDATED_MODEL.key)}",\n "small_model": "custom/small"\n}\n`, path: "/home/test/.config/kilo/kilo.jsonc", }, { @@ -148,8 +165,9 @@ test("scope switch removes only installer-owned stale activation from the old ta await writeScopeManagedConfigs( { managedPaths: context.managedPaths, - model: "qwen3-235b-a22b-instruct-2507-fp8", - previousManagedModelKey: "qwen3-235b-a22b-instruct-2507-fp8", + model: TEST_VALIDATED_MODEL.key, + modelCatalog: context.modelCatalog, + previousManagedModelKey: TEST_VALIDATED_MODEL.key, projectRoot: context.projectRoot, scope: "project", }, @@ -165,3 +183,11 @@ test("scope switch removes only installer-owned stale activation from the old ta assert.match(userConfigText, /"small_model": "custom\/small"/); assert.match(projectConfigText, /"model": "gonkagate\//); }); + +function formatModelRef(modelKey: string): string { + return `gonkagate/${modelKey}`; +} + +function escapeRegExp(value: string): string { + return value.replace(/[.*+?^${}()|[\]\\]/g, "\\$&"); +} diff --git a/test/install/selection.test.ts b/test/install/selection.test.ts index de2ee71..32b167a 100644 --- a/test/install/selection.test.ts +++ b/test/install/selection.test.ts @@ -9,6 +9,7 @@ import { createStubbedTestInstallDependencies } from "./test-deps.js"; import { createEmptyTestModelCatalog, createValidatedTestModelCatalog, + TEST_VALIDATED_MODEL, } from "./test-model-catalog.js"; import { expectInstallErrorCode } from "./test-helpers.js"; @@ -111,7 +112,7 @@ test("resolveInstallScope asks for confirmation when the previous managed scope ); }); -test("resolveInstallModel uses the recommended validated default for --yes", async () => { +test("resolveInstallModel uses the live default for --yes", async () => { const model = await resolveInstallModel( { yes: true, @@ -124,5 +125,5 @@ test("resolveInstallModel uses the recommended validated default for --yes", asy }), ); - assert.equal(model.key, "kimi-k2.6"); + assert.equal(model.key, TEST_VALIDATED_MODEL.key); }); diff --git a/test/install/test-deps.ts b/test/install/test-deps.ts index 8a52449..07c93e7 100644 --- a/test/install/test-deps.ts +++ b/test/install/test-deps.ts @@ -6,13 +6,17 @@ import { type InstallCommandRunner, type InstallDependencies, type InstallFs, + type InstallHttpClient, type InstallInput, type InstallPrompts, type InstallRuntimeEnvironment, type InstallRuntimeOverrides, type InstallSelectOptions, } from "../../src/install/deps.js"; -import type { InstallModelCatalog } from "../../src/install/model-catalog.js"; +import { + createInstallModelCatalog, + type InstallModelCatalog, +} from "../../src/install/model-catalog.js"; import { getInstallPathApi, normalizeInstallPath, @@ -51,6 +55,10 @@ type TestInstallInputConfig = TestDependencyConfig< InstallInput, { stdinText?: string } >; +type TestInstallHttpConfig = TestDependencyConfig< + InstallHttpClient, + { body?: unknown; ok?: boolean; status?: number } +>; type TestInstallModelCatalogConfig = OverrideConfig; type TestInstallPromptsConfig = TestDependencyConfig< InstallPrompts, @@ -68,6 +76,7 @@ export interface TestInstallDependencyOverrides { seedDirectories?: readonly TestInstallFsDirectorySeed[]; seedFiles?: readonly TestInstallFsFileSeed[]; fs?: Partial; + http?: TestInstallHttpConfig; input?: TestInstallInputConfig; models?: TestInstallModelCatalogConfig; prompts?: TestInstallPromptsConfig; @@ -437,6 +446,53 @@ export function createStubInstallInput(stdinText = ""): InstallInput { }; } +export function createStubInstallHttp( + options: { body?: unknown; ok?: boolean; status?: number } = {}, +): InstallHttpClient { + return { + async fetchJson() { + return { + body: options.body ?? { + data: [{ id: "provider/live-test-model", name: "Live Test Model" }], + }, + ok: options.ok ?? true, + status: options.status ?? 200, + }; + }, + }; +} + +export function createStubInstallModelCatalog(): InstallModelCatalog { + return createInstallModelCatalog([ + { + adapterPackage: "@ai-sdk/openai-compatible", + displayName: "Live Test Model", + key: "provider/live-test-model", + limits: { + context: 240000, + output: 8192, + }, + modelId: "provider/live-test-model", + recommended: true, + transport: "chat_completions", + validationStatus: "validated", + }, + { + adapterPackage: "@ai-sdk/openai-compatible", + displayName: "Qwen Fixture", + key: "qwen3-235b-a22b-instruct-2507-fp8", + limits: { + context: 240000, + output: 8192, + }, + modelId: "qwen3-235b-a22b-instruct-2507-fp8", + recommended: false, + transport: "chat_completions", + validationStatus: "validated", + }, + ]); +} + export function createStubInstallPrompts( options: { error?: unknown; @@ -516,6 +572,14 @@ export function createStubbedTestInstallDependencies( overrides.commandBehaviors ?? {}, ), fs: overrides.fs === undefined ? stubFs : { ...stubFs, ...overrides.fs }, + http: + overrides.http?.kind === "override" + ? overrides.http.value + : createStubInstallHttp({ + body: overrides.http?.body, + ok: overrides.http?.ok, + status: overrides.http?.status, + }), input: overrides.input?.kind === "override" ? overrides.input.value @@ -523,7 +587,7 @@ export function createStubbedTestInstallDependencies( models: overrides.models?.kind === "override" ? overrides.models.value - : undefined, + : createStubInstallModelCatalog(), prompts: overrides.prompts?.kind === "override" ? overrides.prompts.value diff --git a/test/install/test-model-catalog.ts b/test/install/test-model-catalog.ts index 56fa01d..2683051 100644 --- a/test/install/test-model-catalog.ts +++ b/test/install/test-model-catalog.ts @@ -1,46 +1,43 @@ -import type { RecommendedProductionDefaultCuratedModel } from "../../src/constants/models.js"; -import type { InstallModelCatalog } from "../../src/install/model-catalog.js"; +import { + createInstallModelCatalog, + type InstallModel, + type InstallModelCatalog, +} from "../../src/install/model-catalog.js"; export const TEST_VALIDATED_MODEL = { adapterPackage: "@ai-sdk/openai-compatible", - displayName: "Kimi K2.6", - key: "kimi-k2.6", + displayName: "Live Test Model", + key: "provider/live-test-model", limits: { context: 240000, output: 8192, }, - modelId: "moonshotai/Kimi-K2.6", + modelId: "provider/live-test-model", recommended: true, transport: "chat_completions", validationStatus: "validated", -} as const satisfies RecommendedProductionDefaultCuratedModel; +} as const satisfies InstallModel; -export function createValidatedTestModelCatalog(): InstallModelCatalog { - return { - getCuratedModelByKey(key) { - return key === TEST_VALIDATED_MODEL.key - ? TEST_VALIDATED_MODEL - : undefined; - }, - getRecommendedProductionDefaultModel() { - return TEST_VALIDATED_MODEL; - }, - getValidatedModels() { - return [TEST_VALIDATED_MODEL]; - }, - }; +export const TEST_EXTRA_MODEL = { + adapterPackage: "@ai-sdk/openai-compatible", + displayName: "Live Extra Model", + key: "provider/live-extra-model", + limits: { + context: 240000, + output: 8192, + }, + modelId: "provider/live-extra-model", + recommended: false, + transport: "chat_completions", + validationStatus: "validated", +} as const satisfies InstallModel; + +export function createValidatedTestModelCatalog( + models: readonly InstallModel[] = [TEST_VALIDATED_MODEL], +): InstallModelCatalog { + return createInstallModelCatalog(models); } export function createEmptyTestModelCatalog(): InstallModelCatalog { - return { - getCuratedModelByKey() { - return undefined; - }, - getRecommendedProductionDefaultModel() { - return undefined; - }, - getValidatedModels() { - return []; - }, - }; + return createInstallModelCatalog([]); } diff --git a/test/install/verification-blockers.test.ts b/test/install/verification-blockers.test.ts index 15f0ff1..a301e99 100644 --- a/test/install/verification-blockers.test.ts +++ b/test/install/verification-blockers.test.ts @@ -2,7 +2,7 @@ import assert from "node:assert/strict"; import test from "node:test"; import { collectManagedOverlapBlockers, - collectMissingCuratedModelEntryBlockers, + collectMissingSelectedModelEntryBlockers, collectProviderActivationBlockers, collectProviderModelFilterBlockers, collectProviderShapeBlockers, @@ -85,9 +85,9 @@ test("provider shape blockers catch non-object provider definitions", () => { ); }); -test("missing curated model entry blockers fire when a higher-precedence provider omits the selected model", () => { +test("missing selected model entry blockers fire when a higher-precedence provider omits the selected model", () => { assert.deepEqual( - collectMissingCuratedModelEntryBlockers( + collectMissingSelectedModelEntryBlockers( { provider: { gonkagate: { @@ -112,7 +112,7 @@ test("missing curated model entry blockers fire when a higher-precedence provide layer: "system_managed_config", path: "/etc/kilo/kilo.jsonc", reason: - "Higher-precedence config defines the GonkaGate provider without the curated model entry.", + "Higher-precedence config defines the GonkaGate provider without the selected model entry.", }, ], ); diff --git a/test/install/verify-effective.test.ts b/test/install/verify-effective.test.ts index c0edd73..fd8c0f9 100644 --- a/test/install/verify-effective.test.ts +++ b/test/install/verify-effective.test.ts @@ -14,14 +14,37 @@ import { createStubbedTestInstallDependencies, type StubInstallFs, } from "./test-deps.js"; +import { + createValidatedTestModelCatalog, + TEST_VALIDATED_MODEL, +} from "./test-model-catalog.js"; + +const VERIFY_MODEL_KEY = "qwen3-235b-a22b-instruct-2507-fp8"; +const VERIFY_MODEL_CATALOG = createValidatedTestModelCatalog([ + TEST_VALIDATED_MODEL, + { + adapterPackage: "@ai-sdk/openai-compatible", + displayName: "Qwen Fixture", + key: VERIFY_MODEL_KEY, + limits: { + context: 240000, + output: 8192, + }, + modelId: VERIFY_MODEL_KEY, + recommended: true, + transport: "chat_completions", + validationStatus: "validated", + }, +]); function createVerifiedConfigDocument() { return JSON.stringify( { - model: formatKiloModelRef("qwen3-235b-a22b-instruct-2507-fp8"), + model: formatKiloModelRef(VERIFY_MODEL_KEY), provider: { gonkagate: buildManagedProviderConfig( - "qwen3-235b-a22b-instruct-2507-fp8", + VERIFY_MODEL_KEY, + VERIFY_MODEL_CATALOG, ), }, }, @@ -55,7 +78,8 @@ test("verifyEffectiveKiloConfig succeeds when local resolver and oracle agree on { provider: { gonkagate: buildManagedProviderConfig( - "qwen3-235b-a22b-instruct-2507-fp8", + VERIFY_MODEL_KEY, + VERIFY_MODEL_CATALOG, ), }, }, @@ -65,9 +89,7 @@ test("verifyEffectiveKiloConfig succeeds when local resolver and oracle agree on path: "/home/test/.config/kilo/kilo.jsonc", }, { - contents: `{ "model": "${formatKiloModelRef( - "qwen3-235b-a22b-instruct-2507-fp8", - )}" }\n`, + contents: `{ "model": "${formatKiloModelRef(VERIFY_MODEL_KEY)}" }\n`, path: "/workspace/project/.kilo/kilo.jsonc", }, ], @@ -82,7 +104,7 @@ test("verifyEffectiveKiloConfig succeeds when local resolver and oracle agree on { kiloCommand: "kilo", managedPaths, - model: "qwen3-235b-a22b-instruct-2507-fp8", + model: VERIFY_MODEL_KEY, projectRoot: dependencies.runtime.cwd, scope: "project", }, @@ -121,7 +143,8 @@ test("verifyEffectiveKiloConfig prefers the detected local kilo oracle command o { provider: { gonkagate: buildManagedProviderConfig( - "qwen3-235b-a22b-instruct-2507-fp8", + VERIFY_MODEL_KEY, + VERIFY_MODEL_CATALOG, ), }, }, @@ -131,9 +154,7 @@ test("verifyEffectiveKiloConfig prefers the detected local kilo oracle command o path: "/home/test/.config/kilo/kilo.jsonc", }, { - contents: `{ "model": "${formatKiloModelRef( - "qwen3-235b-a22b-instruct-2507-fp8", - )}" }\n`, + contents: `{ "model": "${formatKiloModelRef(VERIFY_MODEL_KEY)}" }\n`, path: "/workspace/project/.kilo/kilo.jsonc", }, ], @@ -148,7 +169,7 @@ test("verifyEffectiveKiloConfig prefers the detected local kilo oracle command o { kiloCommand: "kilo", managedPaths, - model: "qwen3-235b-a22b-instruct-2507-fp8", + model: VERIFY_MODEL_KEY, projectRoot: dependencies.runtime.cwd, scope: "project", }, @@ -184,7 +205,8 @@ test("verifyEffectiveKiloConfig keeps oracle sandbox artifacts out of the reposi { provider: { gonkagate: buildManagedProviderConfig( - "qwen3-235b-a22b-instruct-2507-fp8", + VERIFY_MODEL_KEY, + VERIFY_MODEL_CATALOG, ), }, }, @@ -194,9 +216,7 @@ test("verifyEffectiveKiloConfig keeps oracle sandbox artifacts out of the reposi path: "/home/test/.config/kilo/kilo.jsonc", }, { - contents: `{ "model": "${formatKiloModelRef( - "qwen3-235b-a22b-instruct-2507-fp8", - )}" }\n`, + contents: `{ "model": "${formatKiloModelRef(VERIFY_MODEL_KEY)}" }\n`, path: "/workspace/project/.kilo/kilo.jsonc", }, ], @@ -212,7 +232,7 @@ test("verifyEffectiveKiloConfig keeps oracle sandbox artifacts out of the reposi { kiloCommand: "kilo", managedPaths, - model: "qwen3-235b-a22b-instruct-2507-fp8", + model: VERIFY_MODEL_KEY, projectRoot: dependencies.runtime.cwd, scope: "project", }, @@ -255,7 +275,8 @@ test("verifyEffectiveKiloConfig falls back when the runtime temp dir points insi { provider: { gonkagate: buildManagedProviderConfig( - "qwen3-235b-a22b-instruct-2507-fp8", + VERIFY_MODEL_KEY, + VERIFY_MODEL_CATALOG, ), }, }, @@ -265,9 +286,7 @@ test("verifyEffectiveKiloConfig falls back when the runtime temp dir points insi path: "/home/test/.config/kilo/kilo.jsonc", }, { - contents: `{ "model": "${formatKiloModelRef( - "qwen3-235b-a22b-instruct-2507-fp8", - )}" }\n`, + contents: `{ "model": "${formatKiloModelRef(VERIFY_MODEL_KEY)}" }\n`, path: "/workspace/project/.kilo/kilo.jsonc", }, ], @@ -283,7 +302,7 @@ test("verifyEffectiveKiloConfig falls back when the runtime temp dir points insi { kiloCommand: "kilo", managedPaths, - model: "qwen3-235b-a22b-instruct-2507-fp8", + model: VERIFY_MODEL_KEY, projectRoot: dependencies.runtime.cwd, scope: "project", }, @@ -317,7 +336,8 @@ test("verifyCurrentSessionKiloConfig blocks inline secret-binding overrides", as { provider: { gonkagate: buildManagedProviderConfig( - "qwen3-235b-a22b-instruct-2507-fp8", + VERIFY_MODEL_KEY, + VERIFY_MODEL_CATALOG, ), }, }, @@ -327,9 +347,7 @@ test("verifyCurrentSessionKiloConfig blocks inline secret-binding overrides", as path: "/home/test/.config/kilo/kilo.jsonc", }, { - contents: `{ "model": "${formatKiloModelRef( - "qwen3-235b-a22b-instruct-2507-fp8", - )}" }\n`, + contents: `{ "model": "${formatKiloModelRef(VERIFY_MODEL_KEY)}" }\n`, path: "/workspace/project/.kilo/kilo.jsonc", }, ], @@ -344,7 +362,7 @@ test("verifyCurrentSessionKiloConfig blocks inline secret-binding overrides", as verifyCurrentSessionKiloConfig( { managedPaths, - model: "qwen3-235b-a22b-instruct-2507-fp8", + model: VERIFY_MODEL_KEY, projectRoot: dependencies.runtime.cwd, scope: "project", }, @@ -371,7 +389,8 @@ test("verifyEffectiveKiloConfig reports inferred non-local influence when the or model: "custom/other", provider: { gonkagate: buildManagedProviderConfig( - "qwen3-235b-a22b-instruct-2507-fp8", + VERIFY_MODEL_KEY, + VERIFY_MODEL_CATALOG, ), }, }, @@ -392,7 +411,8 @@ test("verifyEffectiveKiloConfig reports inferred non-local influence when the or { provider: { gonkagate: buildManagedProviderConfig( - "qwen3-235b-a22b-instruct-2507-fp8", + VERIFY_MODEL_KEY, + VERIFY_MODEL_CATALOG, ), }, }, @@ -402,9 +422,7 @@ test("verifyEffectiveKiloConfig reports inferred non-local influence when the or path: "/home/test/.config/kilo/kilo.jsonc", }, { - contents: `{ "model": "${formatKiloModelRef( - "qwen3-235b-a22b-instruct-2507-fp8", - )}" }\n`, + contents: `{ "model": "${formatKiloModelRef(VERIFY_MODEL_KEY)}" }\n`, path: "/workspace/project/.kilo/kilo.jsonc", }, ], @@ -420,7 +438,7 @@ test("verifyEffectiveKiloConfig reports inferred non-local influence when the or { kiloCommand: "kilo", managedPaths, - model: "qwen3-235b-a22b-instruct-2507-fp8", + model: VERIFY_MODEL_KEY, projectRoot: dependencies.runtime.cwd, scope: "project", }, diff --git a/test/package-contract.test.ts b/test/package-contract.test.ts index fc0ddc3..c64dc4c 100644 --- a/test/package-contract.test.ts +++ b/test/package-contract.test.ts @@ -6,11 +6,6 @@ import { GONKAGATE_MANAGED_SECRET_PATH, GONKAGATE_PROVIDER_ID, } from "../src/constants/gateway.js"; -import { - CURATED_MODEL_REGISTRY, - getRecommendedProductionDefaultModel, - getValidatedModelKeys, -} from "../src/constants/models.js"; import { buildManagedProviderConfig } from "../src/install/managed-provider-config.js"; import { KILO_CONFIG_CONTENT_ENV_VAR, @@ -27,6 +22,11 @@ import { KILO_SUPPORTED_VERSION_RANGE, } from "../src/install/kilo.js"; import { readText } from "./contract-helpers.js"; +import { + createValidatedTestModelCatalog, + TEST_EXTRA_MODEL, + TEST_VALIDATED_MODEL, +} from "./install/test-model-catalog.js"; interface PackageJson { bin?: Record; @@ -132,70 +132,58 @@ test("constants use Kilo-specific defaults and leave OpenCode env vars out", () assert.doesNotMatch(runtimeSource, /OPENCODE_CONFIG/); }); -test("curated model registry exposes Kimi as the shipped validated default", () => { - const kimi = CURATED_MODEL_REGISTRY["kimi-k2.6"]; - const qwen = CURATED_MODEL_REGISTRY["qwen3-235b-a22b-instruct-2507-fp8"]; - const minimax = CURATED_MODEL_REGISTRY["minimax-m2.7"]; - - assert.equal(kimi.adapterPackage, "@ai-sdk/openai-compatible"); - assert.deepEqual(kimi.limits, { context: 240000, output: 8192 }); - assert.equal(kimi.modelId, "moonshotai/Kimi-K2.6"); - assert.equal(kimi.recommended, true); - assert.equal(kimi.transport, "chat_completions"); - assert.equal(kimi.validationStatus, "validated"); - - assert.equal(qwen.adapterPackage, "@ai-sdk/openai-compatible"); - assert.equal(qwen.limits?.context, 240000); - assert.equal(qwen.limits?.output, 8192); - assert.equal(qwen.modelId, "qwen/qwen3-235b-a22b-instruct-2507-fp8"); - assert.equal(qwen.recommended, false); - assert.equal(qwen.transport, "chat_completions"); - assert.equal(qwen.validationStatus, "validated"); - - assert.equal(minimax.adapterPackage, "@ai-sdk/openai-compatible"); - assert.equal(minimax.limits?.context, 180000); - assert.equal(minimax.limits?.output, 8192); - assert.equal(minimax.modelId, "minimaxai/minimax-m2.7"); - assert.equal(minimax.recommended, false); - assert.equal(minimax.transport, "chat_completions"); - assert.equal(minimax.validationStatus, "validated"); - assert.deepEqual(getValidatedModelKeys(), [ - "kimi-k2.6", - "qwen3-235b-a22b-instruct-2507-fp8", - "minimax-m2.7", +test("managed provider config includes generic Kilo model limits", () => { + const catalog = createValidatedTestModelCatalog([ + TEST_VALIDATED_MODEL, + TEST_EXTRA_MODEL, ]); - assert.equal(getRecommendedProductionDefaultModel()?.key, "kimi-k2.6"); -}); - -test("managed provider config includes the required Kilo model limits", () => { - const providerConfig = buildManagedProviderConfig("kimi-k2.6") as { + const providerConfig = buildManagedProviderConfig( + TEST_VALIDATED_MODEL.key, + catalog, + ) as { models: Record; }; - assert.equal(providerConfig.models["kimi-k2.6"]?.limit?.context, 240000); - assert.equal(providerConfig.models["kimi-k2.6"]?.limit?.output, 8192); - assert.equal(providerConfig.models["minimax-m2.7"]?.limit?.context, 180000); - assert.equal(providerConfig.models["minimax-m2.7"]?.limit?.output, 8192); + assert.equal( + providerConfig.models[TEST_VALIDATED_MODEL.key]?.limit?.context, + 240000, + ); + assert.equal( + providerConfig.models[TEST_VALIDATED_MODEL.key]?.limit?.output, + 8192, + ); + assert.equal( + providerConfig.models[TEST_EXTRA_MODEL.key]?.limit?.output, + 8192, + ); }); -test("managed provider config exposes the validated catalog for model switching", () => { - const providerConfig = buildManagedProviderConfig("kimi-k2.6") as { +test("managed provider config exposes every fetched model for switching", () => { + const catalog = createValidatedTestModelCatalog([ + TEST_VALIDATED_MODEL, + TEST_EXTRA_MODEL, + ]); + const providerConfig = buildManagedProviderConfig( + TEST_VALIDATED_MODEL.key, + catalog, + ) as { models: Record; }; assert.deepEqual(Object.keys(providerConfig.models), [ - "kimi-k2.6", - "qwen3-235b-a22b-instruct-2507-fp8", - "minimax-m2.7", + TEST_VALIDATED_MODEL.key, + TEST_EXTRA_MODEL.key, ]); - assert.equal(providerConfig.models["kimi-k2.6"]?.id, "moonshotai/Kimi-K2.6"); assert.equal( - providerConfig.models["qwen3-235b-a22b-instruct-2507-fp8"]?.id, - "qwen/qwen3-235b-a22b-instruct-2507-fp8", + providerConfig.models[TEST_VALIDATED_MODEL.key]?.id, + TEST_VALIDATED_MODEL.modelId, + ); + assert.equal( + providerConfig.models[TEST_EXTRA_MODEL.key]?.id, + TEST_EXTRA_MODEL.modelId, ); assert.equal( - providerConfig.models["minimax-m2.7"]?.id, - "minimaxai/minimax-m2.7", + providerConfig.models[TEST_EXTRA_MODEL.key]?.name, + TEST_EXTRA_MODEL.displayName, ); - assert.equal(providerConfig.models["minimax-m2.7"]?.name, "MiniMax M2.7"); }); diff --git a/test/readme-contract.test.ts b/test/readme-contract.test.ts index c93ac20..25faf17 100644 --- a/test/readme-contract.test.ts +++ b/test/readme-contract.test.ts @@ -24,6 +24,6 @@ test("README stays usable as the first-stop GitHub and npm landing page", () => assert.match(readme, /~\/\.gonkagate\/kilo\/api-key/); assert.match(readme, /@kilocode\/cli >=7\.2\.0/); assert.match(readme, /chat\/completions/); - assert.match(readme, /moonshotai\/Kimi-K2\.6/); + assert.match(readme, /GET https:\/\/api\.gonkagate\.com\/v1\/models/); assert.match(readme, /no native Windows production claim yet/i); });