diff --git a/AGENTS.md b/AGENTS.md index 08406d9..6557c8d 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -21,9 +21,9 @@ npx @gonkagate/openclaw The happy-path installer interactively prompts only for: - a `gp-...` API key -- a model picker from the curated registry +- a model picker from the live GonkaGate catalog -After the API key is entered, the installer fetches `GET /v1/models` from GonkaGate, requires the live catalog to contain every code-owned curated model, and uses that live metadata to populate the OpenClaw provider model catalog. +After the API key is entered, the installer fetches `GET /v1/models` from GonkaGate, dedupes valid returned model ids in response order, and uses that live response as the source for user-visible model choices and OpenClaw provider model catalog writes. If the active OpenClaw config path does not exist yet, the installer runs `openclaw setup` automatically to initialize the base OpenClaw config and workspace, ensures a minimal local Gateway mode when OpenClaw setup did not already pick one, and then applies GonkaGate-managed settings. @@ -33,7 +33,7 @@ After setup, users can run a read-only verification command: npx @gonkagate/openclaw verify ``` -That command checks the current active OpenClaw config path, confirms that the managed GonkaGate provider fields, curated provider model catalog, curated `/models` allowlist, curated primary model, and owner-only file permissions still match the supported setup, and then verifies that the active local OpenClaw runtime is healthy enough to load that config through read-only CLI probes. +That command checks the current active OpenClaw config path, confirms that the managed GonkaGate provider fields, selected primary model, selected provider model catalog entry, selected `/models` allowlist entry, and owner-only file permissions still match the supported setup, and then verifies that the active local OpenClaw runtime is healthy enough to load that config through read-only CLI probes. If the installer finishes but the local OpenClaw Gateway is not running yet, that is still a successful install outcome. In that case the installer prints one exact next command: @@ -53,10 +53,10 @@ These decisions are part of the repo contract. Changing them is not a small refa - `models.providers.openai.baseUrl` is always `https://api.gonkagate.com/v1` - `models.providers.openai.api` is always `openai-completions` -- `models.providers.openai.models` must always end up as a valid array containing the curated GonkaGate model catalog entries returned by `GET /v1/models`, while preserving unrelated existing entries +- `models.providers.openai.models` must always end up as a valid array containing entries generated from the authenticated live `GET /v1/models` response, while preserving unrelated existing entries - users do not choose the base URL and cannot override it in the public flow - the provider is always `openai`, not `anthropic` -- model choice comes only from a code-owned curated registry +- model choice comes only from the authenticated live `GET /v1/models` response - the primary UX is `npx @gonkagate/openclaw` - the follow-up verification UX is `npx @gonkagate/openclaw verify` - API key entry must remain interactive and hidden @@ -71,16 +71,9 @@ These decisions are part of the repo contract. Changing them is not a small refa - invalid existing JSON5 must stop the installer - config files must be written with owner-only permissions - backup files must be written with owner-only permissions -- `agents.defaults.model.primary` must be updated to the selected curated model -- `agents.defaults.models` must be created or updated with the curated GonkaGate allowlist so OpenClaw `/models` can switch between supported models -- the installer must never expose arbitrary live `GET /v1/models` entries as selectable models unless they are also in the code-owned curated registry - -Current honest limitation: - -- the curated registry currently contains these supported models: - - `qwen3-235b` -> `qwen/qwen3-235b-a22b-instruct-2507-fp8` - - `kimi-k2.6` -> `moonshotai/kimi-k2.6` (default) - - `minimax-m2.7` -> `minimaxai/minimax-m2.7` +- `agents.defaults.model.primary` must be updated to the selected live model +- `agents.defaults.models` must be created or updated with the live GonkaGate model entries so OpenClaw `/models` can switch between returned models +- the installer must never expose or write arbitrary model ids that were not returned by the authenticated live `GET /v1/models` response ## What the Repo Does and Does Not Do @@ -89,7 +82,7 @@ This repo does: - onboarding for local `OpenClaw` - read-only verification for the managed GonkaGate OpenClaw config - read-only runtime verification for the active local OpenClaw Gateway and resolved primary model -- live GonkaGate model catalog validation through `GET /v1/models` before prompting for a model +- live GonkaGate model catalog fetch and validation through `GET /v1/models` before prompting for a model - first-run OpenClaw config bootstrap through `openclaw setup` when needed - first-run minimal local Gateway bootstrap when `gateway.mode` is absent - persistent config writing into the active OpenClaw config path resolved from the current environment @@ -106,7 +99,7 @@ This repo does not do: - shell profile mutation - `.env` writing - custom base URL setup -- arbitrary custom model setup +- arbitrary custom model setup outside the authenticated live GonkaGate catalog - project-local scope in v1 ## Repository Structure @@ -244,7 +237,7 @@ It is responsible for: This is where the fixed product values live: - `gateway.ts` stores the public base URL, API adapter, and provider id -- `models.ts` stores the curated supported model registry and default entry +- `models.ts` stores model-ref and allowlist helpers; it must not contain a user-facing model registry This is one of the most sensitive parts of the repo. Do not add extra configurability here without an explicit product decision. @@ -253,7 +246,7 @@ This is one of the most sensitive parts of the repo. Do not add extra configurab This file contains the interactive prompts built on top of `@inquirer/prompts`: - hidden prompt for API key -- model picker from the curated registry +- model picker from the fetched GonkaGate model catalog The key rule here is: do not log secrets and do not turn the main UX into CLI args for secrets. @@ -266,9 +259,10 @@ It must: - call `https://api.gonkagate.com/v1/models` with the entered `gp-...` key - reject authentication failures before config writes - validate and normalize the external JSON response before the install use-case consumes it -- keep selectable models restricted to the code-owned curated registry -- require the live catalog to contain every curated supported model -- map live metadata into OpenClaw provider model catalog entries without trusting unrelated response fields +- dedupe valid model ids in response order +- reject empty or invalid responses +- keep selectable models restricted to the returned live catalog +- map live model ids and optional names into OpenClaw provider model catalog entries without trusting unrelated response fields ### `src/install/check-openclaw.ts` @@ -341,10 +335,10 @@ It must: - preserve unrelated provider entries - overwrite only OpenClaw-managed `openai` provider fields - preserve unrelated existing `models.providers.openai.models` entries when present -- add or update curated GonkaGate provider model catalog entries under `models.providers.openai.models` +- add or update live GonkaGate provider model catalog entries under `models.providers.openai.models` - set `agents.defaults.model.primary` - preserve unrelated keys inside `agents.defaults.model` -- create or update `agents.defaults.models` with every curated GonkaGate allowlist entry needed by OpenClaw `/models` +- create or update `agents.defaults.models` with every live GonkaGate model entry needed by OpenClaw `/models` It must consume the shared managed-settings boundary instead of re-deriving managed object shapes locally. @@ -415,9 +409,9 @@ It must: - fail if `openclaw config validate --json` rejects the current config - fail if GonkaGate-managed provider fields do not match the fixed product values - fail if `models.providers.openai.apiKey` is missing or malformed -- fail if `agents.defaults.model.primary` does not point at a curated supported model -- fail if `models.providers.openai.models` omits a curated GonkaGate model catalog entry -- fail if `agents.defaults.models` is missing or any managed curated allowlist entry is missing or mismatched +- fail if `agents.defaults.model.primary` is not an `openai/` ref +- fail if `models.providers.openai.models` omits the selected GonkaGate model catalog entry +- fail if `agents.defaults.models` is missing or the selected managed allowlist entry is missing or mismatched - fail if the config file permissions are not owner-only - never rewrite the config during verification @@ -480,8 +474,8 @@ Baseline tests cover: 6. If Gateway mode is still unset after that bootstrap, the installer sets `gateway.mode` to `local` 7. The installer validates the current config through `openclaw config validate --json` 8. The installer securely prompts for a `gp-...` API key -9. The installer fetches `GET /v1/models` and confirms every curated supported model is live -10. The installer shows the curated model picker +9. The installer fetches `GET /v1/models`, dedupes valid returned ids, and rejects an empty catalog +10. The installer shows the live model picker 11. The config is merged with GonkaGate-managed OpenAI settings, provider model catalog entries, and `/models` allowlist entries 12. The generated candidate config is validated through `openclaw config validate --json` 13. A backup is created only when an existing config is being overwritten @@ -496,7 +490,7 @@ Optional follow-up verification path: 3. The CLI resolves the active OpenClaw config path from the current environment 4. The CLI loads that config file without modifying it 5. The CLI validates the current config through `openclaw config validate --json` -6. The CLI verifies the managed GonkaGate provider fields, curated provider model catalog entries, curated `/models` allowlist, curated primary model, and owner-only file permissions +6. The CLI verifies the managed GonkaGate provider fields, selected provider model catalog entry, selected `/models` allowlist entry, selected primary model, and owner-only file permissions 7. The CLI confirms that the local OpenClaw Gateway RPC is reachable and the health snapshot is healthy 8. The CLI confirms that OpenClaw resolves the expected primary model through `openclaw models status --plain` 9. The CLI reports success or exits with a clear error @@ -506,7 +500,7 @@ Optional follow-up verification path: - Do not add a base URL prompt - Do not add free-form custom model input - Do not make `--api-key` a recommended or supported path -- Do not expose arbitrary `GET /v1/models` results as selectable models outside the curated registry +- Do not expose or write model ids that are absent from the authenticated `GET /v1/models` response - Do not require `openclaw onboard` in the main public flow - Do not modify shell rc files - Do not write `.env` @@ -575,14 +569,14 @@ Pause and double-check if your change touches: - OpenClaw config format - backup and restore behavior - default provider wiring -- the curated model registry +- the live GonkaGate model catalog boundary - the public install command ## Repo Philosophy This repository should stay onboarding-first. -It is not a general-purpose OpenClaw provider configurator and not a playground for dozens of options. Its value is that users get one obvious, short, and safe path: install OpenClaw, run this package, enter a `gp-...` key, choose a curated model, and then keep using OpenClaw normally. +It is not a general-purpose OpenClaw provider configurator and not a playground for dozens of options. Its value is that users get one obvious, short, and safe path: install OpenClaw, run this package, enter a `gp-...` key, choose a live GonkaGate model, and then keep using OpenClaw normally. @RTK.md diff --git a/CHANGELOG.md b/CHANGELOG.md index c96cfbd..be1329b 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -28,10 +28,9 @@ ## [Unreleased] -- Added `minimaxai/minimax-m2.7` to the curated GonkaGate model registry under the `minimax-m2.7` model key. -- Added `moonshotai/kimi-k2.6` to the curated GonkaGate model registry under the `kimi-k2.6` model key and made it the default model. -- The installer now fetches GonkaGate `GET /v1/models` after API key entry, requires every curated model to be live, and uses the live metadata for OpenClaw provider model catalog entries. -- The installer now creates or updates `agents.defaults.models` with the curated GonkaGate allowlist so OpenClaw `/models` can switch between supported models. +- The installer now uses GonkaGate `GET /v1/models` after API key entry as the source of user-visible model choices and config writes, instead of a checked-in curated model registry. +- `--model` is now validated against the authenticated live catalog, and the model picker defaults to the first returned live model id. +- The installer now creates or updates `agents.defaults.models` with live GonkaGate model entries so OpenClaw `/models` can switch between returned models. - Raised the minimum supported Node.js runtime for this package to Node 22.14+ so it matches current OpenClaw install support expectations. - CI and publish workflows now both run on Node 22.14.0, and runtime documentation no longer advertises Node 18 support. - Upgraded `@inquirer/prompts`, `commander`, and `write-file-atomic` to current releases that are now appropriate for a Node 22.14+ baseline. @@ -45,7 +44,7 @@ - Installer runtime probes now distinguish a not-yet-running local Gateway from real runtime mismatches; when the Gateway is simply not running yet, install succeeds and prints `openclaw gateway` as the exact next step. - Runtime verification now treats malformed or shape-drifted OpenClaw probe output as a strict compatibility failure instead of downgrading it to a benign Gateway-not-running result. - The installer now validates both the current config and the generated candidate config through `openclaw config validate --json` before writing. -- The managed OpenAI provider config now preserves unrelated existing `models.providers.openai.models` entries while adding or updating curated GonkaGate catalog entries required by OpenClaw's model picker. +- The managed OpenAI provider config now preserves unrelated existing `models.providers.openai.models` entries while adding or updating live GonkaGate catalog entries required by OpenClaw's model picker. - The CLI now resolves the active config path compatibly with stable OpenClaw 2026.4.1 by preferring existing legacy config candidates locally before falling back to canonical `openclaw.json`, so install and verify stay aligned on legacy hosts. ## [0.1.0] - 2026-04-01 diff --git a/README.md b/README.md index 517be5f..1141277 100644 --- a/README.md +++ b/README.md @@ -26,7 +26,7 @@ npx @gonkagate/openclaw You will be asked for: - your GonkaGate API key (`gp-...`) in a hidden interactive prompt -- a model from the curated GonkaGate registry after the installer confirms the live catalog through `GET /v1/models` +- a model from the live GonkaGate catalog returned by `GET /v1/models` If the active OpenClaw config path does not exist yet, the installer will run `openclaw setup` automatically, ensure a minimal local Gateway mode, and then apply the GonkaGate-specific provider settings. @@ -53,9 +53,9 @@ This command checks: - that `models.providers.openai.baseUrl` is `https://api.gonkagate.com/v1` - that `models.providers.openai.api` is `openai-completions` - that `models.providers.openai.apiKey` exists and still looks like a `gp-...` key -- that `models.providers.openai.models` includes the curated GonkaGate model catalog entries needed by OpenClaw -- that `agents.defaults.model.primary` points at a curated GonkaGate model -- that `agents.defaults.models` contains the curated GonkaGate switcher allowlist used by OpenClaw `/models` +- that `agents.defaults.model.primary` is an `openai/` GonkaGate model ref +- that `models.providers.openai.models` includes the selected model id +- that `agents.defaults.models` contains the selected model allowlist entry used by OpenClaw `/models` - that the config file still uses owner-only permissions - that the local OpenClaw Gateway RPC is reachable through `openclaw gateway status --require-rpc --json` - that `openclaw health --json` reports a healthy runtime @@ -63,13 +63,9 @@ This command checks: `verify` is still read-only, but it now expects the local OpenClaw Gateway to be running so it can prove the saved config is actually active. -## Supported Models +## Model Catalog -Current curated registry in this package: - -- `qwen3-235b` -> `qwen/qwen3-235b-a22b-instruct-2507-fp8` -- `kimi-k2.6` -> `moonshotai/kimi-k2.6` (default) -- `minimax-m2.7` -> `minimaxai/minimax-m2.7` +This package does not ship a hardcoded user-facing model catalog. After the hidden API key prompt, it calls GonkaGate `GET /v1/models`, dedupes returned model ids in response order, and uses that live list for the picker and config writes. Pressing Enter in the picker accepts the first returned model. ## What It Does @@ -88,14 +84,14 @@ It also: - bootstraps `gateway.mode: "local"` on true first-run installs when OpenClaw has not already set a gateway mode - refuses to overwrite an invalid JSON5 config - refuses to overwrite a config that fails current OpenClaw schema validation -- fetches `GET /v1/models` with the entered GonkaGate API key, requires every curated model to be present, and keeps model selection inside the code-owned curated registry +- fetches `GET /v1/models` with the entered GonkaGate API key and keeps model selection inside the returned live catalog - creates a backup before overwriting an existing config - preserves unrelated top-level config keys - preserves other provider entries under `models.providers` - overwrites only the managed `models.providers.openai` fields -- merges live curated GonkaGate entries into `models.providers.openai.models` while preserving unrelated existing entries -- sets `agents.defaults.model.primary` to the chosen curated model -- creates or updates `agents.defaults.models` with the curated GonkaGate switcher allowlist so `/models` can switch between supported models +- merges live GonkaGate entries into `models.providers.openai.models` while preserving unrelated existing entries +- sets `agents.defaults.model.primary` to the chosen live model +- creates or updates `agents.defaults.models` with live GonkaGate switcher entries so `/models` can switch between returned models - validates the generated config with `openclaw config validate --json` before replacing the live file - writes the config atomically with owner-only permissions - writes backup files with owner-only permissions @@ -109,9 +105,9 @@ This installer manages only these OpenClaw surfaces: - `models.providers.openai.baseUrl` - `models.providers.openai.apiKey` - `models.providers.openai.api` -- `models.providers.openai.models` as a valid array containing the live curated GonkaGate model catalog, while preserving unrelated existing entries +- `models.providers.openai.models` as a valid array containing the live GonkaGate model catalog, while preserving unrelated existing entries - `agents.defaults.model.primary` -- `agents.defaults.models["openai/"].alias` for curated GonkaGate models +- `agents.defaults.models["openai/"].alias` for live GonkaGate models Everything else is left intact. @@ -124,7 +120,7 @@ These parts are intentionally fixed in the public onboarding flow: - API adapter: `openai-completions` - API key entry: interactive only -This tool does not ask for a custom base URL and does not accept arbitrary model IDs. +This tool does not ask for a custom base URL and does not accept model IDs that are absent from the authenticated GonkaGate `/v1/models` response. ## Verify diff --git a/docs/how-it-works.md b/docs/how-it-works.md index c0e81a1..0eb5161 100644 --- a/docs/how-it-works.md +++ b/docs/how-it-works.md @@ -20,9 +20,9 @@ Install flow: 5. On true first-run installs only, ensure `gateway.mode` is set to `"local"` when OpenClaw setup did not already choose a gateway mode. 6. Stop with a clear error if the current config is invalid, the managed config surface has an unsafe shape, or `openclaw config validate --json` rejects the current file. 7. Prompt for the GonkaGate API key in a hidden interactive prompt. -8. Fetch the live GonkaGate catalog through `GET /v1/models` with that API key and require it to contain every curated in-code model. -9. Prompt for a model from the curated in-code registry using live catalog metadata for the provider model entries. -10. Merge only the managed OpenAI provider fields, the live curated `models.providers.openai.models` catalog entries, `agents.defaults.model.primary`, and the curated `agents.defaults.models` switcher allowlist. +8. Fetch the live GonkaGate catalog through `GET /v1/models` with that API key, dedupe valid model ids in response order, and reject an empty catalog. +9. Prompt for a model from that live catalog. +10. Merge only the managed OpenAI provider fields, the live `models.providers.openai.models` catalog entries, `agents.defaults.model.primary`, and the live `agents.defaults.models` switcher entries. 11. Validate the generated config through `openclaw config validate --json` against a temporary candidate file next to the live config. 12. Create a timestamped backup next to the existing config file only when overwriting an existing config. 13. Write the resulting config atomically with owner-only permissions. @@ -37,9 +37,9 @@ Verify flow: 5. Confirm that `models.providers.openai.baseUrl` still points to `https://api.gonkagate.com/v1`. 6. Confirm that `models.providers.openai.api` is still `openai-completions`. 7. Confirm that `models.providers.openai.apiKey` exists and still looks like a GonkaGate `gp-...` key. -8. Confirm that `agents.defaults.model.primary` still points at one of the curated GonkaGate models. -9. Confirm that `models.providers.openai.models` includes every curated GonkaGate model id needed by OpenClaw's model catalog. -10. Confirm that `agents.defaults.models` contains every curated GonkaGate allowlist entry and alias needed by `/models`. +8. Confirm that `agents.defaults.model.primary` is an `openai/` ref. +9. Confirm that `models.providers.openai.models` includes the selected GonkaGate model id. +10. Confirm that `agents.defaults.models` contains the selected model allowlist entry and alias needed by `/models`. 11. Confirm that the config file still uses owner-only permissions. 12. Confirm that the local OpenClaw Gateway RPC is reachable through `openclaw gateway status --require-rpc --json`. 13. Confirm that `openclaw health --json` reports a healthy runtime snapshot. @@ -51,9 +51,9 @@ Managed merge behavior: - Other `models.providers.*` entries are preserved. - Existing `models.providers.openai` keys outside the managed surface are preserved. - Existing `models.providers.openai.models` entries are preserved when already present and valid. -- Live curated GonkaGate entries from `GET /v1/models` are added to `models.providers.openai.models`, or updated in place when their `id` already exists. +- Live GonkaGate entries from `GET /v1/models` are added to `models.providers.openai.models`, or updated in place when their `id` already exists. - Existing `agents.defaults.model` keys outside `primary` are preserved. -- Existing `agents.defaults.models` entries are preserved, and curated GonkaGate entries are created or updated so `/models` can switch between supported models. +- Existing `agents.defaults.models` entries are preserved, and live GonkaGate entries are created or updated so `/models` can switch between returned models. - Existing `gateway.*` keys are preserved. - Existing `gateway.mode` is preserved when already present. - Only true first-run installs gain a default `gateway.mode: "local"` when the base setup omitted it. diff --git a/docs/troubleshooting.md b/docs/troubleshooting.md index c4f48bd..9b80095 100644 --- a/docs/troubleshooting.md +++ b/docs/troubleshooting.md @@ -146,7 +146,8 @@ The installer checks the live GonkaGate model catalog after you enter the `gp-.. - `GonkaGate rejected the API key` means the key could not call `GET /v1/models`; check the key and rerun the installer. - `model catalog is temporarily unavailable` means GonkaGate returned `503`; rerun after a short wait. -- `did not return every curated supported model` means the live catalog did not contain every model this package supports, so the installer stopped instead of writing a partial `/models` switcher. +- `returned no usable models` means the live catalog did not include any valid model ids, so the installer stopped before writing config. +- `Selected model "..." was not returned` means the `--model` id you supplied is not currently present in the authenticated live catalog. ## `Expected "models.providers.openai.baseUrl"...` or other GonkaGate field mismatch errors diff --git a/src/cli.ts b/src/cli.ts index 7826697..4e15fce 100644 --- a/src/cli.ts +++ b/src/cli.ts @@ -2,20 +2,14 @@ import process from "node:process"; import { pathToFileURL } from "node:url"; import { Command, CommanderError, Option } from "commander"; import { GONKAGATE_OPENAI_API, GONKAGATE_OPENAI_BASE_URL } from "./constants/gateway.js"; -import { - DEFAULT_MODEL_KEY, - SUPPORTED_MODELS, - SUPPORTED_MODEL_KEYS -} from "./constants/models.js"; import type { CliDisplay } from "./install/cli-display.js"; import { InstallError, CliUsageError } from "./install/install-errors.js"; import { runInstallUseCase as runInstallUseCaseImpl } from "./install/install-use-case.js"; import { getSettingsTarget as getSettingsTargetImpl } from "./install/settings-paths.js"; import { runVerifyUseCase as runVerifyUseCaseImpl } from "./install/verify-use-case.js"; -import type { SupportedModelKey } from "./constants/models.js"; interface CliOptions { - modelKey?: SupportedModelKey; + modelId?: string; } interface CliRequest extends CliOptions { @@ -23,7 +17,7 @@ interface CliRequest extends CliOptions { } interface ParsedProgramOptions { - model?: SupportedModelKey; + model?: string; } interface ProgramOutput { @@ -58,16 +52,11 @@ function rejectApiKeyArgs(argv: string[]): void { } function createProgram(handlers: CliHandlers, output?: ProgramOutput): Command { - const supportedModelLines = SUPPORTED_MODELS.map((model) => { - const defaultSuffix = model.key === DEFAULT_MODEL_KEY ? " (default)" : ""; - return ` ${model.key} ${model.displayName}${defaultSuffix}`; - }).join("\n"); - const program = new Command() .name("gonkagate-openclaw") .description("GonkaGate OpenClaw installer and config verifier") .addOption( - new Option("--model ", "Skip the model prompt with a curated supported model.").choices(SUPPORTED_MODEL_KEYS) + new Option("--model ", "Skip the model prompt with a model id returned by GonkaGate /v1/models.") ) .action(handlers.onInstall) .helpOption("-h, --help", "Show this help.") @@ -77,11 +66,8 @@ function createProgram(handlers: CliHandlers, output?: ProgramOutput): Command { ` Examples: npx @gonkagate/openclaw - npx @gonkagate/openclaw --model ${DEFAULT_MODEL_KEY} + npx @gonkagate/openclaw --model provider/model-id npx @gonkagate/openclaw verify - -Supported model keys: -${supportedModelLines} ` ) .exitOverride(); @@ -109,7 +95,7 @@ export function parseCliRequest(argv: string[], output?: ProgramOutput): CliRequ onInstall: (options) => { request = { command: "install", - modelKey: options.model + modelId: options.model }; }, onVerify: () => { @@ -127,7 +113,7 @@ export function parseCliRequest(argv: string[], output?: ProgramOutput): CliRequ export function parseCliOptions(argv: string[], output?: ProgramOutput): CliOptions { return { - modelKey: parseCliRequest(argv, output).modelKey + modelId: parseCliRequest(argv, output).modelId }; } @@ -137,7 +123,7 @@ function printIntro(targetPath: string): void { console.log(`Target config: ${targetPath}`); console.log(`Managed provider: models.providers.openai -> ${GONKAGATE_OPENAI_BASE_URL}`); console.log(`Managed API adapter: ${GONKAGATE_OPENAI_API}`); - console.log(`Curated model choice: ${SUPPORTED_MODEL_KEYS.join(", ")}.\n`); + console.log("Model catalog: fetched from GonkaGate after API key entry.\n"); } function printVerifyIntro(targetPath: string): void { @@ -169,9 +155,9 @@ async function runInstall( cliDependencies: CliDependencies ): Promise { printIntro(targetPath); - const request = options.modelKey + const request = options.modelId ? { - modelKey: options.modelKey, + modelId: options.modelId, targetPath } : { diff --git a/src/constants/models.ts b/src/constants/models.ts index 86150c6..79db98b 100644 --- a/src/constants/models.ts +++ b/src/constants/models.ts @@ -1,104 +1,50 @@ import { OPENCLAW_PROVIDER_ID } from "./gateway.js"; -export interface SupportedModelDefinition { - key: string; +export interface GonkaGateModel { displayName: string; + key: string; modelId: string; - description?: string; - isDefault?: boolean; -} - -const curatedModelRegistry = [ - { - key: "qwen3-235b", - displayName: "Qwen 3 235B Instruct", - modelId: "qwen/qwen3-235b-a22b-instruct-2507-fp8", - description: "Qwen reasoning model on GonkaGate.", - isDefault: false - }, - { - key: "kimi-k2.6", - displayName: "Kimi K2.6", - modelId: "moonshotai/kimi-k2.6", - description: "Recommended default for long-horizon coding and agentic workflows on GonkaGate.", - isDefault: true - }, - { - key: "minimax-m2.7", - displayName: "MiniMax M2.7", - modelId: "minimaxai/minimax-m2.7", - description: "MiniMax model on GonkaGate.", - isDefault: false - } -] as const satisfies readonly SupportedModelDefinition[]; - -type CuratedModelRegistry = typeof curatedModelRegistry; - -const defaultModels = curatedModelRegistry.filter((model) => model.isDefault); - -if (defaultModels.length !== 1) { - throw new Error(`Expected exactly one default supported model, found ${defaultModels.length}.`); } -export const SUPPORTED_MODELS = curatedModelRegistry; -export type SupportedModel = CuratedModelRegistry[number]; -export type SupportedModelKey = SupportedModel["key"]; -export type SupportedPrimaryModelRef = `${typeof OPENCLAW_PROVIDER_ID}/${SupportedModel["modelId"]}`; -export const DEFAULT_MODEL: SupportedModel = defaultModels[0]; -export const DEFAULT_MODEL_KEY: SupportedModelKey = DEFAULT_MODEL.key; -export const SUPPORTED_MODEL_KEYS: SupportedModelKey[] = Array.from( - SUPPORTED_MODELS, - (model): SupportedModelKey => model.key -); +export type GonkaGateModelKey = string; +export type GonkaGatePrimaryModelRef = `${typeof OPENCLAW_PROVIDER_ID}/${string}`; export interface ManagedAllowlistEntry { - alias: SupportedModelKey; + alias: string; } export interface ManagedModelSelection { allowlistEntry: ManagedAllowlistEntry; - primaryModelRef: SupportedPrimaryModelRef; - selectedModel: SupportedModel; -} - -export function getSupportedModelByKey(key: string): SupportedModel | undefined { - return SUPPORTED_MODELS.find((model) => model.key === key); + primaryModelRef: GonkaGatePrimaryModelRef; + selectedModel: GonkaGateModel; } -export function requireSupportedModel(key: string): SupportedModel { - const model = getSupportedModelByKey(key); - - if (!model) { - throw new Error(`Unsupported model key "${key}". Supported model keys: ${SUPPORTED_MODEL_KEYS.join(", ")}`); - } - - return model; -} - -export function toPrimaryModelRef(model: SupportedModel): SupportedPrimaryModelRef { +export function toPrimaryModelRef(model: GonkaGateModel): GonkaGatePrimaryModelRef { return `${OPENCLAW_PROVIDER_ID}/${model.modelId}`; } -export function toManagedModelSelection(selectedModel: SupportedModel): ManagedModelSelection { +export function toManagedModelSelection(selectedModel: GonkaGateModel): ManagedModelSelection { return { allowlistEntry: { - alias: selectedModel.key + alias: selectedModel.modelId }, primaryModelRef: toPrimaryModelRef(selectedModel), selectedModel }; } -export function getSupportedModelByPrimaryRef(primaryModelRef: string): SupportedModel | undefined { - return getManagedModelSelectionByPrimaryRef(primaryModelRef)?.selectedModel; -} +export function modelFromPrimaryRef(primaryModelRef: string, displayName?: string): GonkaGateModel | undefined { + const prefix = `${OPENCLAW_PROVIDER_ID}/`; -export function getManagedModelSelectionByPrimaryRef(primaryModelRef: string): ManagedModelSelection | undefined { - const selectedModel = SUPPORTED_MODELS.find((model) => toPrimaryModelRef(model) === primaryModelRef); + if (!primaryModelRef.startsWith(prefix) || primaryModelRef.length === prefix.length) { + return undefined; + } - return selectedModel ? toManagedModelSelection(selectedModel) : undefined; -} + const modelId = primaryModelRef.slice(prefix.length); -export function listSupportedPrimaryModelRefs(): SupportedPrimaryModelRef[] { - return Array.from(SUPPORTED_MODELS, (model): SupportedPrimaryModelRef => toPrimaryModelRef(model)); + return { + displayName: displayName ?? modelId, + key: modelId, + modelId + }; } diff --git a/src/install/cli-display.ts b/src/install/cli-display.ts index 7b63c32..1e84fcf 100644 --- a/src/install/cli-display.ts +++ b/src/install/cli-display.ts @@ -1,4 +1,4 @@ -import type { SupportedModel } from "../constants/models.js"; +import type { GonkaGateModel } from "../constants/models.js"; import { formatUnixMode } from "./file-permissions.js"; export interface CliDisplaySection { @@ -25,14 +25,14 @@ interface InstallDisplayInput { kind: "healthy"; resolvedPrimaryModelRef: string; }; - selectedModel: SupportedModel; + selectedModel: GonkaGateModel; targetPath: string; } interface VerifyDisplayInput { configMode: number; resolvedPrimaryModelRef: string; - selectedModel: SupportedModel; + selectedModel: GonkaGateModel; targetPath: string; } diff --git a/src/install/gonkagate-models.ts b/src/install/gonkagate-models.ts index 9f78746..563ff36 100644 --- a/src/install/gonkagate-models.ts +++ b/src/install/gonkagate-models.ts @@ -1,12 +1,11 @@ import { setTimeout as delay } from "node:timers/promises"; import { GONKAGATE_OPENAI_BASE_URL } from "../constants/gateway.js"; import { - SUPPORTED_MODELS, toManagedModelSelection, - type ManagedAllowlistEntry, - type SupportedModel, - type SupportedModelKey, - type SupportedPrimaryModelRef + type GonkaGateModel, + type GonkaGateModelKey, + type GonkaGatePrimaryModelRef, + type ManagedAllowlistEntry } from "../constants/models.js"; import { GonkaGateModelsError, describeValue, getErrorMessage } from "./install-errors.js"; import { asPlainObject } from "./object-utils.js"; @@ -28,112 +27,78 @@ export interface FetchGonkaGateModelsOptions { timeoutMs?: number; } -export interface GonkaGateModelCatalogEntry { - contextLength?: number; +export interface LiveGonkaGateModel { id: string; name?: string; } export interface OpenClawProviderModelCatalogEntry { - contextWindow?: number; id: string; name: string; } -export interface CuratedGonkaGateModelCatalogEntry { +export interface GonkaGateModelCatalogEntry { allowlistEntry: ManagedAllowlistEntry; - model: SupportedModel; - primaryModelRef: SupportedPrimaryModelRef; + model: GonkaGateModel; + primaryModelRef: GonkaGatePrimaryModelRef; providerModel: OpenClawProviderModelCatalogEntry; } interface GonkaGateModelCatalog { - models: readonly GonkaGateModelCatalogEntry[]; + models: readonly LiveGonkaGateModel[]; } -export async function fetchCuratedGonkaGateModelCatalog( +export async function fetchGonkaGateModelCatalog( apiKey: string, options: FetchGonkaGateModelsOptions = {} -): Promise { - const catalog = await fetchGonkaGateModelCatalog(apiKey, options); - const curatedCatalog = createCuratedGonkaGateModelCatalog(catalog.models); - const curatedModelIds = new Set(curatedCatalog.map((entry) => entry.model.modelId)); - const missingSupportedModels = SUPPORTED_MODELS.filter((model) => !curatedModelIds.has(model.modelId)); - - if (curatedCatalog.length === 0) { - throw new GonkaGateModelsError({ - expected: Array.from(SUPPORTED_MODELS, (model) => model.modelId).join(", "), - kind: "no_supported_models", - message: - `GonkaGate ${GONKAGATE_MODELS_ENDPOINT} did not return any curated supported models. ` + - `Expected at least one of: ${Array.from(SUPPORTED_MODELS, (model) => model.modelId).join(", ")}.` - }); - } - - if (missingSupportedModels.length > 0) { - throw new GonkaGateModelsError({ - actual: catalog.models.map((model) => model.id).join(", "), - expected: Array.from(SUPPORTED_MODELS, (model) => model.modelId).join(", "), - kind: "missing_supported_models", - message: - `GonkaGate ${GONKAGATE_MODELS_ENDPOINT} did not return every curated supported model. ` + - `Missing: ${missingSupportedModels.map((model) => model.modelId).join(", ")}.` - }); - } - - return curatedCatalog; +): Promise { + const catalog = await requestGonkaGateModelCatalog(apiKey, options); + return createGonkaGateModelCatalog(catalog.models); } -export function createStaticCuratedGonkaGateModelCatalog( - models: readonly SupportedModel[] = SUPPORTED_MODELS -): CuratedGonkaGateModelCatalogEntry[] { - return Array.from(models, (model) => createCuratedCatalogEntry(model)); +export function createGonkaGateModelCatalog( + liveModels: readonly LiveGonkaGateModel[] +): GonkaGateModelCatalogEntry[] { + return liveModels.map((liveModel) => createCatalogEntry(liveModel)); } export function requireModelInGonkaGateCatalog( - selectedModel: SupportedModel, - catalog: readonly CuratedGonkaGateModelCatalogEntry[] -): void { - if (catalog.some((entry) => entry.model.key === selectedModel.key)) { - return; + modelId: string, + catalog: readonly GonkaGateModelCatalogEntry[] +): GonkaGateModel { + const selectedModel = catalog.find((entry) => entry.model.modelId === modelId)?.model; + + if (selectedModel) { + return selectedModel; } throw new GonkaGateModelsError({ - actual: selectedModel.modelId, - expected: catalog.length > 0 - ? catalog.map((entry) => entry.model.modelId).join(", ") - : Array.from(SUPPORTED_MODELS, (model) => model.modelId).join(", "), + actual: modelId, + expected: catalog.map((entry) => entry.model.modelId).join(", "), kind: "missing_selected_model", message: - `Selected curated model "${selectedModel.key}" (${selectedModel.modelId}) was not returned by ` + - `GonkaGate ${GONKAGATE_MODELS_ENDPOINT}. Choose a currently available curated model and rerun the installer.` + `Selected model "${modelId}" was not returned by GonkaGate ${GONKAGATE_MODELS_ENDPOINT}. ` + + "Choose a currently available model and rerun the installer." }); } export function getPromptDefaultModelKey( - catalog: readonly CuratedGonkaGateModelCatalogEntry[], - preferredDefaultKey: SupportedModelKey -): SupportedModelKey { - const preferredDefault = catalog.find((entry) => entry.model.key === preferredDefaultKey); - - if (preferredDefault) { - return preferredDefault.model.key; - } - + catalog: readonly GonkaGateModelCatalogEntry[] +): GonkaGateModelKey { const firstAvailable = catalog[0]; if (!firstAvailable) { throw new GonkaGateModelsError({ - expected: Array.from(SUPPORTED_MODELS, (model) => model.modelId).join(", "), - kind: "no_supported_models", - message: "No curated GonkaGate models are available for the model prompt." + expected: "at least one model id", + kind: "empty_catalog", + message: "No GonkaGate models are available for the model prompt." }); } return firstAvailable.model.key; } -async function fetchGonkaGateModelCatalog( +async function requestGonkaGateModelCatalog( apiKey: string, options: FetchGonkaGateModelsOptions ): Promise { @@ -236,12 +201,32 @@ function parseGonkaGateModelCatalog(value: unknown): GonkaGateModelCatalog { throw invalidCatalogResponse("data", "array", root.data); } + const seenIds = new Set(); + const models: LiveGonkaGateModel[] = []; + + for (const [index, entry] of root.data.entries()) { + const model = parseGonkaGateModelEntry(entry, index); + + if (!seenIds.has(model.id)) { + seenIds.add(model.id); + models.push(model); + } + } + + if (models.length === 0) { + throw new GonkaGateModelsError({ + expected: "at least one model id", + kind: "empty_catalog", + message: `GonkaGate ${GONKAGATE_MODELS_ENDPOINT} returned no usable models.` + }); + } + return { - models: root.data.map((entry, index) => parseGonkaGateModelEntry(entry, index)) + models }; } -function parseGonkaGateModelEntry(value: unknown, index: number): GonkaGateModelCatalogEntry { +function parseGonkaGateModelEntry(value: unknown, index: number): LiveGonkaGateModel { const raw = asPlainObject(value); const fieldPrefix = `data[${index}]`; @@ -253,13 +238,12 @@ function parseGonkaGateModelEntry(value: unknown, index: number): GonkaGateModel throw invalidCatalogResponse(`${fieldPrefix}.id`, "non-empty string", raw.id); } + const id = raw.id.trim(); const name = parseOptionalNonEmptyString(raw.name, `${fieldPrefix}.name`); - const contextLength = parseOptionalPositiveInteger(raw.context_length, `${fieldPrefix}.context_length`); return { - id: raw.id, - ...(name ? { name } : {}), - ...(contextLength ? { contextLength } : {}) + id, + ...(name ? { name } : {}) }; } @@ -277,18 +261,6 @@ function parseOptionalNonEmptyString(value: unknown, fieldPath: string): string return trimmed.length > 0 ? trimmed : undefined; } -function parseOptionalPositiveInteger(value: unknown, fieldPath: string): number | undefined { - if (value === undefined || value === null) { - return undefined; - } - - if (typeof value !== "number" || !Number.isSafeInteger(value) || value <= 0) { - throw invalidCatalogResponse(fieldPath, "positive integer", value); - } - - return value; -} - function invalidCatalogResponse(fieldPath: string, expected: string, actualValue: unknown): GonkaGateModelsError<"invalid_response"> { return new GonkaGateModelsError({ actual: describeValue(actualValue), @@ -300,22 +272,12 @@ function invalidCatalogResponse(fieldPath: string, expected: string, actualValue }); } -function createCuratedGonkaGateModelCatalog( - liveModels: readonly GonkaGateModelCatalogEntry[] -): CuratedGonkaGateModelCatalogEntry[] { - const liveById = new Map(liveModels.map((model) => [model.id, model])); - - return SUPPORTED_MODELS.flatMap((model) => { - const liveModel = liveById.get(model.modelId); - - return liveModel ? [createCuratedCatalogEntry(model, liveModel)] : []; - }); -} - -function createCuratedCatalogEntry( - model: SupportedModel, - liveModel?: GonkaGateModelCatalogEntry -): CuratedGonkaGateModelCatalogEntry { +function createCatalogEntry(liveModel: LiveGonkaGateModel): GonkaGateModelCatalogEntry { + const model = { + displayName: liveModel.name ?? liveModel.id, + key: liveModel.id, + modelId: liveModel.id + }; const modelSelection = toManagedModelSelection(model); return { @@ -323,9 +285,8 @@ function createCuratedCatalogEntry( model, primaryModelRef: modelSelection.primaryModelRef, providerModel: { - id: model.modelId, - name: liveModel?.name ?? model.displayName, - ...(liveModel?.contextLength ? { contextWindow: liveModel.contextLength } : {}) + id: liveModel.id, + name: liveModel.name ?? liveModel.id } }; } diff --git a/src/install/install-errors.ts b/src/install/install-errors.ts index ee95eea..ae52146 100644 --- a/src/install/install-errors.ts +++ b/src/install/install-errors.ts @@ -29,13 +29,12 @@ export type ApiKeyValidationKind = "missing" | "wrong_prefix" | "invalid_format" export type GonkaGateModelsFailureKind = | "authentication_failed" | "catalog_unavailable" + | "empty_catalog" | "invalid_response" - | "missing_supported_models" | "missing_selected_model" - | "no_supported_models" | "request_failed"; export type OpenClawConfigValidationKind = "command_failed" | "invalid_config" | "unexpected_validated_path"; -export type PromptFailureKind = "cancelled" | "missing_tty" | "model_registry_mismatch" | "no_supported_models"; +export type PromptFailureKind = "cancelled" | "missing_tty" | "model_catalog_mismatch" | "no_models"; export type SettingsMissingKind = "post_setup_target_missing" | "target_config_missing"; export type SettingsShapeKind = "expected_array" | "expected_non_empty_string" | "expected_object" | "root_not_object"; export type SettingsVerificationKind = diff --git a/src/install/install-use-case.ts b/src/install/install-use-case.ts index 8589c9f..91810b9 100644 --- a/src/install/install-use-case.ts +++ b/src/install/install-use-case.ts @@ -1,14 +1,14 @@ -import { DEFAULT_MODEL_KEY, requireSupportedModel, toPrimaryModelRef } from "../constants/models.js"; -import type { SupportedModel, SupportedModelKey } from "../constants/models.js"; +import { toPrimaryModelRef } from "../constants/models.js"; +import type { GonkaGateModel } from "../constants/models.js"; import type { OpenClawConfig } from "../types/settings.js"; import { createBackup as createBackupImpl } from "./backup.js"; import { ensureFreshInstallLocalGateway as ensureFreshInstallLocalGatewayImpl } from "./bootstrap-gateway.js"; import { createInstallSuccessDisplay, type CliDisplay } from "./cli-display.js"; import { - fetchCuratedGonkaGateModelCatalog as fetchCuratedGonkaGateModelCatalogImpl, + fetchGonkaGateModelCatalog as fetchGonkaGateModelCatalogImpl, getPromptDefaultModelKey, requireModelInGonkaGateCatalog, - type CuratedGonkaGateModelCatalogEntry + type GonkaGateModelCatalogEntry } from "./gonkagate-models.js"; import { loadSettings as loadSettingsImpl, requireLoadedSettings } from "./load-settings.js"; import { mergeSettingsWithGonkaGate } from "./merge-settings.js"; @@ -26,7 +26,7 @@ export interface InstallConfigPreparationResult { } export interface InstallRequest { - modelKey?: SupportedModelKey; + modelId?: string; targetPath: string; } @@ -35,14 +35,14 @@ export interface InstallOutcome { configPreparation: InstallConfigPreparationResult; display: CliDisplay; runtime: InstallRuntimeCheckResult; - selectedModel: SupportedModel; + selectedModel: GonkaGateModel; targetPath: string; } export interface InstallUseCaseDependencies { createBackup: typeof createBackupImpl; ensureFreshInstallLocalGateway: typeof ensureFreshInstallLocalGatewayImpl; - fetchCuratedGonkaGateModelCatalog: typeof fetchCuratedGonkaGateModelCatalogImpl; + fetchGonkaGateModelCatalog: typeof fetchGonkaGateModelCatalogImpl; loadSettings: typeof loadSettingsImpl; openClaw: Pick< OpenClawFacade, @@ -57,7 +57,7 @@ export interface InstallUseCaseDependencies { export const defaultInstallUseCaseDependencies = { createBackup: createBackupImpl, ensureFreshInstallLocalGateway: ensureFreshInstallLocalGatewayImpl, - fetchCuratedGonkaGateModelCatalog: fetchCuratedGonkaGateModelCatalogImpl, + fetchGonkaGateModelCatalog: fetchGonkaGateModelCatalogImpl, loadSettings: loadSettingsImpl, openClaw: createOpenClawFacade(), promptForApiKey: promptForApiKeyImpl, @@ -76,8 +76,8 @@ export async function runInstallUseCase( dependencies.openClaw.validateConfig(request.targetPath); const apiKey = dependencies.validateApiKey(await dependencies.promptForApiKey()); - const modelCatalog = await dependencies.fetchCuratedGonkaGateModelCatalog(apiKey); - const selectedModel = await selectInstallModel(request.modelKey, modelCatalog, dependencies); + const modelCatalog = await dependencies.fetchGonkaGateModelCatalog(apiKey); + const selectedModel = await selectInstallModel(request.modelId, modelCatalog, dependencies); const mergedSettings = mergeSettingsWithGonkaGate(configPreparation.settings, apiKey, selectedModel, modelCatalog); await dependencies.openClaw.validateCandidateConfig(request.targetPath, mergedSettings); @@ -108,18 +108,16 @@ export async function runInstallUseCase( } async function selectInstallModel( - modelKey: SupportedModelKey | undefined, - modelCatalog: readonly CuratedGonkaGateModelCatalogEntry[], + modelId: string | undefined, + modelCatalog: readonly GonkaGateModelCatalogEntry[], dependencies: Pick -): Promise { - if (modelKey) { - const selectedModel = requireSupportedModel(modelKey); - requireModelInGonkaGateCatalog(selectedModel, modelCatalog); - return selectedModel; +): Promise { + if (modelId) { + return requireModelInGonkaGateCatalog(modelId, modelCatalog); } const availableModels = modelCatalog.map((entry) => entry.model); - const defaultModelKey = getPromptDefaultModelKey(modelCatalog, DEFAULT_MODEL_KEY); + const defaultModelKey = getPromptDefaultModelKey(modelCatalog); return dependencies.promptForModel(availableModels, defaultModelKey); } diff --git a/src/install/managed-settings-access.ts b/src/install/managed-settings-access.ts index aeb00c0..59780c1 100644 --- a/src/install/managed-settings-access.ts +++ b/src/install/managed-settings-access.ts @@ -1,5 +1,4 @@ import { OPENCLAW_PROVIDER_ID } from "../constants/gateway.js"; -import { listSupportedPrimaryModelRefs } from "../constants/models.js"; import type { OpenClawConfig } from "../types/settings.js"; import { SettingsShapeError, describeValue } from "./install-errors.js"; import { @@ -80,7 +79,7 @@ export function readManagedSettingsView(settings: OpenClawConfig, sourceLabel: s const defaultModel = readManagedDefaultModelView(defaults?.model, sourceLabel); const allowlist = requirePlainObjectWhenPresent(defaults?.models, MANAGED_SETTINGS_PATHS.allowlist, sourceLabel); - for (const primaryModelRef of listSupportedPrimaryModelRefs()) { + for (const primaryModelRef of Object.keys(allowlist ?? {})) { void readManagedAllowlistEntryWhenPresent(allowlist, primaryModelRef, sourceLabel); } diff --git a/src/install/merge-settings.ts b/src/install/merge-settings.ts index 73d2d47..25b9fd6 100644 --- a/src/install/merge-settings.ts +++ b/src/install/merge-settings.ts @@ -1,9 +1,8 @@ import { GONKAGATE_OPENAI_API, GONKAGATE_OPENAI_BASE_URL, OPENCLAW_PROVIDER_ID } from "../constants/gateway.js"; -import type { ManagedAllowlistEntry, SupportedModel } from "../constants/models.js"; +import type { GonkaGateModel, ManagedAllowlistEntry } from "../constants/models.js"; import type { OpenClawConfig } from "../types/settings.js"; import { - createStaticCuratedGonkaGateModelCatalog, - type CuratedGonkaGateModelCatalogEntry, + type GonkaGateModelCatalogEntry, type OpenClawProviderModelCatalogEntry } from "./gonkagate-models.js"; import { readManagedAllowlistEntryWhenPresent, readManagedSettingsView } from "./managed-settings-access.js"; @@ -19,8 +18,8 @@ import { export function mergeSettingsWithGonkaGate( settings: OpenClawConfig, apiKey: string, - selectedModel: SupportedModel, - modelCatalog: readonly CuratedGonkaGateModelCatalogEntry[] = createStaticCuratedGonkaGateModelCatalog() + selectedModel: GonkaGateModel, + modelCatalog: readonly GonkaGateModelCatalogEntry[] ): OpenClawConfig { const managedSettings = readManagedSettingsView(settings, "the loaded OpenClaw config"); const selectedModelState = requireCatalogEntryForSelectedModel(selectedModel, modelCatalog); @@ -64,13 +63,13 @@ export function mergeSettingsWithGonkaGate( } function requireCatalogEntryForSelectedModel( - selectedModel: SupportedModel, - modelCatalog: readonly CuratedGonkaGateModelCatalogEntry[] -): CuratedGonkaGateModelCatalogEntry { - const selectedModelState = modelCatalog.find((entry) => entry.model.key === selectedModel.key); + selectedModel: GonkaGateModel, + modelCatalog: readonly GonkaGateModelCatalogEntry[] +): GonkaGateModelCatalogEntry { + const selectedModelState = modelCatalog.find((entry) => entry.model.modelId === selectedModel.modelId); if (!selectedModelState) { - throw new Error(`Selected model "${selectedModel.key}" is missing from the GonkaGate model catalog.`); + throw new Error(`Selected model "${selectedModel.modelId}" is missing from the GonkaGate model catalog.`); } return selectedModelState; @@ -79,7 +78,7 @@ function requireCatalogEntryForSelectedModel( function mergeManagedAllowlistEntries( allowlist: PlainObject, existingAllowlist: ReadonlyPlainObject | undefined, - modelCatalog: readonly CuratedGonkaGateModelCatalogEntry[] + modelCatalog: readonly GonkaGateModelCatalogEntry[] ): PlainObject { let mergedAllowlist = allowlist; @@ -116,7 +115,7 @@ function mergeManagedAllowlistEntry( function mergeOpenAiProviderModelCatalog( existingModels: unknown[], - modelCatalog: readonly CuratedGonkaGateModelCatalogEntry[] + modelCatalog: readonly GonkaGateModelCatalogEntry[] ): unknown[] { const mergedModels = [...existingModels]; diff --git a/src/install/prompts.ts b/src/install/prompts.ts index 950f30c..a6ef7b5 100644 --- a/src/install/prompts.ts +++ b/src/install/prompts.ts @@ -1,6 +1,6 @@ import process from "node:process"; import { password, select } from "@inquirer/prompts"; -import type { SupportedModel, SupportedModelKey } from "../constants/models.js"; +import type { GonkaGateModel, GonkaGateModelKey } from "../constants/models.js"; import { PromptError } from "./install-errors.js"; export async function promptForApiKey(): Promise { @@ -36,11 +36,11 @@ interface SelectPromptConfig { type SelectPrompt = (config: SelectPromptConfig) => Promise; export function buildModelPromptConfig( - models: readonly SupportedModel[], - defaultModelKey: SupportedModelKey -): SelectPromptConfig { + models: readonly GonkaGateModel[], + defaultModelKey: GonkaGateModelKey +): SelectPromptConfig { if (models.length === 0) { - throw new PromptError("no_supported_models", "No supported GonkaGate models are configured."); + throw new PromptError("no_models", "No GonkaGate models are available."); } const defaultModel = requireModel(models, defaultModelKey); @@ -52,7 +52,7 @@ export function buildModelPromptConfig( value: model.key, name: model.displayName, short: model.key, - description: `${model.description ? `${model.description} ` : ""}Model ID: ${model.modelId}` + description: `Model ID: ${model.modelId}` })), pageSize: Math.min(models.length, 8), loop: false, @@ -63,21 +63,21 @@ export function buildModelPromptConfig( } export async function promptForModel( - models: readonly SupportedModel[], - defaultModelKey: SupportedModelKey, - selectPrompt: SelectPrompt = select as SelectPrompt -): Promise { + models: readonly GonkaGateModel[], + defaultModelKey: GonkaGateModelKey, + selectPrompt: SelectPrompt = select as SelectPrompt +): Promise { const selectedModelKey = await selectPrompt(buildModelPromptConfig(models, defaultModelKey)).catch(rethrowPromptExit); return requireModel(models, selectedModelKey); } -function requireModel(models: readonly SupportedModel[], key: SupportedModelKey): SupportedModel { +function requireModel(models: readonly GonkaGateModel[], key: GonkaGateModelKey): GonkaGateModel { const selectedModel = models.find((model) => model.key === key); if (!selectedModel) { throw new PromptError( - "model_registry_mismatch", - `Configured model "${key}" is not present in the curated model registry.` + "model_catalog_mismatch", + `Selected model "${key}" is not present in the fetched GonkaGate model catalog.` ); } diff --git a/src/install/verify-settings.ts b/src/install/verify-settings.ts index b8a8d85..371eb7b 100644 --- a/src/install/verify-settings.ts +++ b/src/install/verify-settings.ts @@ -1,13 +1,11 @@ import { stat } from "node:fs/promises"; import { GONKAGATE_OPENAI_API, GONKAGATE_OPENAI_BASE_URL } from "../constants/gateway.js"; import { - getManagedModelSelectionByPrimaryRef, - listSupportedPrimaryModelRefs, - SUPPORTED_MODELS, - toPrimaryModelRef, + modelFromPrimaryRef, + toManagedModelSelection, + type GonkaGateModel, type ManagedModelSelection } from "../constants/models.js"; -import type { SupportedModel } from "../constants/models.js"; import type { OpenClawConfig } from "../types/settings.js"; import { MANAGED_SETTINGS_PATHS, @@ -18,12 +16,12 @@ import { } from "./managed-settings-access.js"; import { DEFAULT_OWNER_ONLY_MODE, formatUnixMode, hasOwnerOnlyPermissions } from "./file-permissions.js"; import { SettingsVerificationError, describeValue, getErrorMessage } from "./install-errors.js"; -import type { ReadonlyPlainObject } from "./object-utils.js"; +import { asPlainObject, type ReadonlyPlainObject } from "./object-utils.js"; import { validateApiKey } from "./validate-api-key.js"; export interface VerifySettingsResult { configMode: number; - selectedModel: SupportedModel; + selectedModel: GonkaGateModel; } export async function verifySettings(filePath: string, settings: OpenClawConfig): Promise { @@ -57,21 +55,10 @@ export async function verifySettings(filePath: string, settings: OpenClawConfig) }); } - const selectedModelState = getManagedModelSelectionByPrimaryRef(primaryModelRef); + const selectedModel = requireSelectedModel(primaryModelRef, providerModels, filePath); + const selectedModelState = toManagedModelSelection(selectedModel); - if (!selectedModelState) { - throw new SettingsVerificationError({ - actual: primaryModelRef, - expected: listSupportedPrimaryModelRefs().join(", "), - fieldPath: MANAGED_SETTINGS_PATHS.primaryModel, - filePath, - kind: "mismatched_managed_value", - message: `Expected "${MANAGED_SETTINGS_PATHS.primaryModel}" in ${filePath} to be one of: ${listSupportedPrimaryModelRefs().join(", ")}.` - }); - } - - verifyOpenAiProviderModelCatalog(providerModels, filePath); - verifyModelAllowlist(managed.allowlist, filePath); + verifyModelAllowlist(managed.allowlist, filePath, selectedModelState); let configMode: number; @@ -98,7 +85,7 @@ export async function verifySettings(filePath: string, settings: OpenClawConfig) return { configMode, - selectedModel: selectedModelState.selectedModel + selectedModel }; } @@ -156,7 +143,8 @@ function requireManagedApiKey(value: unknown, filePath: string): string { function verifyModelAllowlist( allowlist: ReadonlyPlainObject | undefined, - filePath: string + filePath: string, + selectedModelState: ManagedModelSelection ): void { if (!allowlist) { throw new SettingsVerificationError({ @@ -164,13 +152,11 @@ function verifyModelAllowlist( filePath, kind: "missing_managed_value", message: - `Expected "${MANAGED_SETTINGS_PATHS.allowlist}" in ${filePath} to exist so OpenClaw /models can list curated GonkaGate models.` + `Expected "${MANAGED_SETTINGS_PATHS.allowlist}" in ${filePath} to exist so OpenClaw /models can list the selected GonkaGate model.` }); } - for (const model of SUPPORTED_MODELS) { - verifyModelAllowlistEntry(allowlist, filePath, getRequiredManagedModelSelection(model)); - } + verifyModelAllowlistEntry(allowlist, filePath, selectedModelState); } function verifyModelAllowlistEntry( @@ -210,39 +196,44 @@ function verifyModelAllowlistEntry( } } -function verifyOpenAiProviderModelCatalog(providerModels: readonly unknown[], filePath: string): void { - for (const model of SUPPORTED_MODELS) { - const expectedModelId = model.modelId; - const modelEntry = providerModels.find((entry) => { - const objectEntry = typeof entry === "object" && entry !== null && !Array.isArray(entry) - ? entry as ReadonlyPlainObject - : undefined; +function requireSelectedModel( + primaryModelRef: string, + providerModels: readonly unknown[], + filePath: string +): GonkaGateModel { + const selectedModel = modelFromPrimaryRef(primaryModelRef); - return objectEntry?.id === expectedModelId; + if (!selectedModel) { + throw new SettingsVerificationError({ + actual: primaryModelRef, + expected: "openai/", + fieldPath: MANAGED_SETTINGS_PATHS.primaryModel, + filePath, + kind: "mismatched_managed_value", + message: `Expected "${MANAGED_SETTINGS_PATHS.primaryModel}" in ${filePath} to be an "openai/" ref.` }); - - if (!modelEntry) { - throw new SettingsVerificationError({ - expected: expectedModelId, - fieldPath: MANAGED_SETTINGS_PATHS.openaiModels, - filePath, - kind: "missing_provider_model_entry", - message: - `Expected "${MANAGED_SETTINGS_PATHS.openaiModels}" in ${filePath} to include model id "${expectedModelId}" ` + - "so OpenClaw /models can expose the curated GonkaGate catalog." - }); - } } -} -function getRequiredManagedModelSelection(model: SupportedModel): ManagedModelSelection { - const modelSelection = getManagedModelSelectionByPrimaryRef(toPrimaryModelRef(model)); + const providerModel = providerModels.map((entry) => asPlainObject(entry)).find((entry) => entry?.id === selectedModel.modelId); - if (!modelSelection) { - throw new Error(`Curated model "${model.key}" does not resolve to a managed OpenClaw model ref.`); + if (!providerModel) { + throw new SettingsVerificationError({ + expected: selectedModel.modelId, + fieldPath: MANAGED_SETTINGS_PATHS.openaiModels, + filePath, + kind: "missing_provider_model_entry", + message: + `Expected "${MANAGED_SETTINGS_PATHS.openaiModels}" in ${filePath} to include model id "${selectedModel.modelId}" ` + + "so OpenClaw /models can expose the selected GonkaGate model." + }); } - return modelSelection; + return { + ...selectedModel, + displayName: typeof providerModel.name === "string" && providerModel.name.trim().length > 0 + ? providerModel.name.trim() + : selectedModel.displayName + }; } function requireNonEmptyString(value: unknown, fieldPath: string, filePath: string): string { diff --git a/test/cli-run.test.ts b/test/cli-run.test.ts index d66c695..d5a95c9 100644 --- a/test/cli-run.test.ts +++ b/test/cli-run.test.ts @@ -1,10 +1,10 @@ import assert from "node:assert/strict"; import test from "node:test"; import { formatCliErrorMessage, parseCliOptions, parseCliRequest, run } from "../src/cli.js"; -import { DEFAULT_MODEL, DEFAULT_MODEL_KEY, toPrimaryModelRef } from "../src/constants/models.js"; +import { toPrimaryModelRef } from "../src/constants/models.js"; import { CliUsageError, RuntimeVerificationError } from "../src/install/install-errors.js"; import type { InstallOutcome } from "../src/install/install-use-case.js"; -import { withCapturedConsoleLog } from "./test-helpers.js"; +import { TEST_MODEL, withCapturedConsoleLog } from "./test-helpers.js"; const silentOutput = { writeOut: () => {}, @@ -39,15 +39,15 @@ function createGatewayUnavailableInstallResult(): InstallOutcome { kind: "healthy", resolvedPrimaryModelRef: "openai/not-used-by-cli" }, - selectedModel: DEFAULT_MODEL, + selectedModel: TEST_MODEL, targetPath: "/tmp/openclaw.json" }; } -test("parseCliOptions accepts supported --model values and rejects unsupported ones", () => { - assert.equal(parseCliOptions(["--model", DEFAULT_MODEL_KEY], silentOutput).modelKey, DEFAULT_MODEL_KEY); - assert.equal(parseCliOptions([`--model=${DEFAULT_MODEL_KEY}`], silentOutput).modelKey, DEFAULT_MODEL_KEY); - assert.throws(() => parseCliOptions(["--model", "not-supported"], silentOutput), /Allowed choices are/); +test("parseCliOptions accepts --model values for live validation after catalog fetch", () => { + assert.equal(parseCliOptions(["--model", TEST_MODEL.modelId], silentOutput).modelId, TEST_MODEL.modelId); + assert.equal(parseCliOptions([`--model=${TEST_MODEL.modelId}`], silentOutput).modelId, TEST_MODEL.modelId); + assert.equal(parseCliOptions(["--model", "not-yet-known/model"], silentOutput).modelId, "not-yet-known/model"); }); test("parseCliRequest routes the verify subcommand separately from the install flow", () => { @@ -76,7 +76,7 @@ test("parseCliOptions rejects --api-key arguments", () => { }); test("run delegates install requests to the install use case with the resolved target path", async () => { - let capturedRequest: { modelKey?: string; targetPath: string } | undefined; + let capturedRequest: { modelId?: string; targetPath: string } | undefined; let verifyCalls = 0; await withCapturedConsoleLog(async () => { @@ -122,8 +122,8 @@ test("run delegates verify requests to the verify use case with the resolved tar } ] }, - resolvedPrimaryModelRef: toPrimaryModelRef(DEFAULT_MODEL), - selectedModel: DEFAULT_MODEL, + resolvedPrimaryModelRef: toPrimaryModelRef(TEST_MODEL), + selectedModel: TEST_MODEL, targetPath }; } @@ -167,7 +167,7 @@ test("run prints verification success details from the verify use case outcome", }, resolvedPrimaryModelRef: "not-used-by-cli", selectedModel: { - ...DEFAULT_MODEL, + ...TEST_MODEL, displayName: "Not used by CLI" }, targetPath @@ -178,7 +178,7 @@ test("run prints verification success details from the verify use case outcome", assert.equal(logs.some((line) => line.includes("Verification complete.")), true); assert.equal(logs.some((line) => line.includes("Permissions: 0o600")), true); assert.equal(logs.some((line) => line.includes("display-owned-model")), true); - assert.equal(logs.some((line) => line.includes(toPrimaryModelRef(DEFAULT_MODEL))), false); + assert.equal(logs.some((line) => line.includes(toPrimaryModelRef(TEST_MODEL))), false); }); test("run stops before any use case when resolving the target path fails", async () => { @@ -202,8 +202,8 @@ test("run stops before any use case when resolving the target path fails", async display: { sections: [] }, - resolvedPrimaryModelRef: toPrimaryModelRef(DEFAULT_MODEL), - selectedModel: DEFAULT_MODEL, + resolvedPrimaryModelRef: toPrimaryModelRef(TEST_MODEL), + selectedModel: TEST_MODEL, targetPath: "/tmp/openclaw.json" }; } diff --git a/test/gonkagate-models.test.ts b/test/gonkagate-models.test.ts index edf3dca..70536eb 100644 --- a/test/gonkagate-models.test.ts +++ b/test/gonkagate-models.test.ts @@ -1,18 +1,17 @@ import assert from "node:assert/strict"; import test from "node:test"; -import { DEFAULT_MODEL, requireSupportedModel } from "../src/constants/models.js"; import { GonkaGateModelsError } from "../src/install/install-errors.js"; import { - fetchCuratedGonkaGateModelCatalog, + fetchGonkaGateModelCatalog, getPromptDefaultModelKey, requireModelInGonkaGateCatalog } from "../src/install/gonkagate-models.js"; -test("fetchCuratedGonkaGateModelCatalog fetches and maps live curated model metadata", async () => { +test("fetchGonkaGateModelCatalog fetches, dedupes, and maps arbitrary live model ids", async () => { let capturedUrl: string | undefined; let capturedAuthorization: string | undefined; - const catalog = await fetchCuratedGonkaGateModelCatalog("gp-test-key", { + const catalog = await fetchGonkaGateModelCatalog("gp-test-key", { fetchImpl: async (url, init) => { capturedUrl = url; capturedAuthorization = init.headers.Authorization; @@ -23,24 +22,22 @@ test("fetchCuratedGonkaGateModelCatalog fetches and maps live curated model meta object: "list", data: [ { - id: "moonshotai/kimi-k2.6", - name: "Kimi K2.6 Live", + id: "acme/model-alpha", + name: "Acme Model Alpha", object: "model" }, { - id: "unsupported/provider-model", - name: "Unsupported", + id: "globex/model-beta", + name: "Globex Model Beta", object: "model" }, { - id: "minimaxai/minimax-m2.7", - name: "MiniMax M2.7 Live", + id: "acme/model-alpha", + name: "Duplicate Alpha", object: "model" }, { - context_length: 240000, - id: "qwen/qwen3-235b-a22b-instruct-2507-fp8", - name: "Qwen3 235B A22B Instruct 2507 FP8", + id: "initech/model-gamma", object: "model" } ] @@ -52,26 +49,33 @@ test("fetchCuratedGonkaGateModelCatalog fetches and maps live curated model meta assert.equal(capturedUrl, "https://api.gonkagate.com/v1/models"); assert.equal(capturedAuthorization, "Bearer gp-test-key"); - assert.deepEqual(catalog.map((entry) => entry.model.key), ["qwen3-235b", "kimi-k2.6", "minimax-m2.7"]); - assert.deepEqual(catalog[0]?.providerModel, { - contextWindow: 240000, - id: "qwen/qwen3-235b-a22b-instruct-2507-fp8", - name: "Qwen3 235B A22B Instruct 2507 FP8" - }); - assert.deepEqual(catalog[1]?.providerModel, { - id: "moonshotai/kimi-k2.6", - name: "Kimi K2.6 Live" - }); - assert.deepEqual(catalog[2]?.providerModel, { - id: "minimaxai/minimax-m2.7", - name: "MiniMax M2.7 Live" - }); + assert.deepEqual(catalog.map((entry) => entry.model.modelId), [ + "acme/model-alpha", + "globex/model-beta", + "initech/model-gamma" + ]); + assert.deepEqual(catalog.map((entry) => entry.providerModel), [ + { + id: "acme/model-alpha", + name: "Acme Model Alpha" + }, + { + id: "globex/model-beta", + name: "Globex Model Beta" + }, + { + id: "initech/model-gamma", + name: "initech/model-gamma" + } + ]); + assert.equal(getPromptDefaultModelKey(catalog), "acme/model-alpha"); + assert.equal(requireModelInGonkaGateCatalog("globex/model-beta", catalog).displayName, "Globex Model Beta"); }); -test("fetchCuratedGonkaGateModelCatalog retries temporary catalog unavailability", async () => { +test("fetchGonkaGateModelCatalog retries temporary catalog unavailability", async () => { let calls = 0; - const catalog = await fetchCuratedGonkaGateModelCatalog("gp-test-key", { + const catalog = await fetchGonkaGateModelCatalog("gp-test-key", { fetchImpl: async () => { calls += 1; @@ -87,13 +91,7 @@ test("fetchCuratedGonkaGateModelCatalog retries temporary catalog unavailability json: async () => ({ data: [ { - id: DEFAULT_MODEL.modelId - }, - { - id: "qwen/qwen3-235b-a22b-instruct-2507-fp8" - }, - { - id: "minimaxai/minimax-m2.7" + id: "acme/model-alpha" } ] }) @@ -104,12 +102,12 @@ test("fetchCuratedGonkaGateModelCatalog retries temporary catalog unavailability }); assert.equal(calls, 2); - assert.deepEqual(catalog.map((entry) => entry.model.key), ["qwen3-235b", DEFAULT_MODEL.key, "minimax-m2.7"]); + assert.deepEqual(catalog.map((entry) => entry.model.modelId), ["acme/model-alpha"]); }); -test("fetchCuratedGonkaGateModelCatalog rejects invalid API keys before config writes", async () => { +test("fetchGonkaGateModelCatalog rejects invalid API keys before config writes", async () => { await assert.rejects( - fetchCuratedGonkaGateModelCatalog("gp-bad-key", { + fetchGonkaGateModelCatalog("gp-bad-key", { fetchImpl: async () => ({ status: 401, json: async () => ({ @@ -129,9 +127,9 @@ test("fetchCuratedGonkaGateModelCatalog rejects invalid API keys before config w ); }); -test("fetchCuratedGonkaGateModelCatalog rejects malformed model catalog responses", async () => { +test("fetchGonkaGateModelCatalog rejects malformed model catalog responses", async () => { await assert.rejects( - fetchCuratedGonkaGateModelCatalog("gp-test-key", { + fetchGonkaGateModelCatalog("gp-test-key", { fetchImpl: async () => ({ status: 200, json: async () => ({ @@ -153,52 +151,42 @@ test("fetchCuratedGonkaGateModelCatalog rejects malformed model catalog response ); }); -test("fetchCuratedGonkaGateModelCatalog rejects catalogs that omit a curated supported model", async () => { +test("fetchGonkaGateModelCatalog rejects empty model catalog responses", async () => { await assert.rejects( - fetchCuratedGonkaGateModelCatalog("gp-test-key", { + fetchGonkaGateModelCatalog("gp-test-key", { fetchImpl: async () => ({ status: 200, json: async () => ({ - data: [ - { - id: "qwen/qwen3-235b-a22b-instruct-2507-fp8" - } - ] + data: [] }) }), maxAttempts: 1 }), (error) => { assert.ok(error instanceof GonkaGateModelsError); - assert.equal(error.kind, "missing_supported_models"); - assert.match(error.message, /moonshotai\/kimi-k2\.6/); - assert.match(error.message, /minimaxai\/minimax-m2\.7/); + assert.equal(error.kind, "empty_catalog"); return true; } ); }); -test("model selection helpers keep selected models inside the live curated catalog", () => { - const qwen = requireSupportedModel("qwen3-235b"); - const kimi = requireSupportedModel("kimi-k2.6"); - const catalog = [ - { - allowlistEntry: { - alias: qwen.key - }, - model: qwen, - primaryModelRef: "openai/qwen/qwen3-235b-a22b-instruct-2507-fp8" as const, - providerModel: { - id: qwen.modelId, - name: qwen.displayName - } - } - ]; +test("model selection helper rejects only the explicitly missing selected live id", async () => { + const catalog = await fetchGonkaGateModelCatalog("gp-test-key", { + fetchImpl: async () => ({ + status: 200, + json: async () => ({ + data: [ + { + id: "acme/model-alpha" + } + ] + }) + }), + maxAttempts: 1 + }); - assert.equal(getPromptDefaultModelKey(catalog, kimi.key), qwen.key); - assert.doesNotThrow(() => requireModelInGonkaGateCatalog(qwen, catalog)); assert.throws( - () => requireModelInGonkaGateCatalog(kimi, catalog), + () => requireModelInGonkaGateCatalog("missing/model", catalog), (error) => { assert.ok(error instanceof GonkaGateModelsError); assert.equal(error.kind, "missing_selected_model"); diff --git a/test/install-use-case.test.ts b/test/install-use-case.test.ts index 2a1396e..8e26393 100644 --- a/test/install-use-case.test.ts +++ b/test/install-use-case.test.ts @@ -1,13 +1,13 @@ import assert from "node:assert/strict"; import test from "node:test"; -import { DEFAULT_MODEL, DEFAULT_MODEL_KEY, requireSupportedModel, toPrimaryModelRef } from "../src/constants/models.js"; +import { toPrimaryModelRef } from "../src/constants/models.js"; import { GonkaGateModelsError, InstallError } from "../src/install/install-errors.js"; -import { createStaticCuratedGonkaGateModelCatalog } from "../src/install/gonkagate-models.js"; import type { OpenClawConfig } from "../src/types/settings.js"; import { runInstallUseCase, type InstallUseCaseDependencies } from "../src/install/install-use-case.js"; +import { TEST_MODEL, TEST_MODEL_BETA, createTestModelCatalog } from "./test-helpers.js"; interface RuntimeVerifyInput { expectedPrimaryModelRef: string; @@ -62,7 +62,7 @@ function recordStep(state: InstallHarnessState, step: string): void { function createHealthyRuntimeResult() { return { kind: "healthy" as const, - resolvedPrimaryModelRef: toPrimaryModelRef(DEFAULT_MODEL) + resolvedPrimaryModelRef: toPrimaryModelRef(TEST_MODEL) }; } @@ -73,18 +73,18 @@ function createGatewayUnavailableInstallResult() { }; } -function createStaticModelCatalog() { - return createStaticCuratedGonkaGateModelCatalog(); +function createLiveModelCatalog() { + return createTestModelCatalog(); } function expectedAllowlist() { return Object.fromEntries( - createStaticModelCatalog().map((entry) => [entry.primaryModelRef, entry.allowlistEntry]) + createLiveModelCatalog().map((entry) => [entry.primaryModelRef, entry.allowlistEntry]) ); } function expectedProviderModels() { - return createStaticModelCatalog().map((entry) => entry.providerModel); + return createLiveModelCatalog().map((entry) => entry.providerModel); } function createInstallHarness( @@ -162,10 +162,10 @@ function createInstallHarness( recordStep(state, "bootstrapGateway"); return createGatewayBootstrapResult(withLocalGatewayMode(settings), true); }, - fetchCuratedGonkaGateModelCatalog: async () => { + fetchGonkaGateModelCatalog: async () => { state.fetchModelCatalogCalls += 1; recordStep(state, "fetchModelCatalog"); - return createStaticModelCatalog(); + return createLiveModelCatalog(); }, loadSettings: async (filePath) => { state.loadCalls += 1; @@ -185,7 +185,7 @@ function createInstallHarness( promptForModel: async () => { state.promptModelCalls += 1; recordStep(state, "promptModel"); - return DEFAULT_MODEL; + return TEST_MODEL; }, validateApiKey: (apiKey) => { state.validateCalls += 1; @@ -259,8 +259,8 @@ test("runInstallUseCase initializes missing OpenClaw config automatically and sk verifyRuntimeForInstall: () => createGatewayUnavailableInstallResult() }, promptForApiKey: async () => "gp-test-key", - fetchCuratedGonkaGateModelCatalog: async () => createStaticModelCatalog(), - promptForModel: async () => DEFAULT_MODEL, + fetchGonkaGateModelCatalog: async () => createLiveModelCatalog(), + promptForModel: async () => TEST_MODEL, validateApiKey: (apiKey) => apiKey, writeSettings: async (_filePath, settings) => { writtenSettings = settings as Record; @@ -281,7 +281,7 @@ test("runInstallUseCase initializes missing OpenClaw config automatically and sk workspace: "~/.openclaw/workspace", models: expectedAllowlist(), model: { - primary: toPrimaryModelRef(DEFAULT_MODEL) + primary: toPrimaryModelRef(TEST_MODEL) } } }, @@ -345,8 +345,8 @@ test("runInstallUseCase preserves an existing gateway.mode from fresh OpenClaw s verifyRuntimeForInstall: () => createGatewayUnavailableInstallResult() }, promptForApiKey: async () => "gp-test-key", - fetchCuratedGonkaGateModelCatalog: async () => createStaticModelCatalog(), - promptForModel: async () => DEFAULT_MODEL, + fetchGonkaGateModelCatalog: async () => createLiveModelCatalog(), + promptForModel: async () => TEST_MODEL, validateApiKey: (apiKey) => apiKey, writeSettings: async (_filePath, settings) => { writtenSettings = settings as Record; @@ -426,7 +426,7 @@ test("runInstallUseCase uses a resolved legacy target path consistently across l assert.deepEqual(state.writePaths, [targetPath]); assert.deepEqual(state.runtimeVerifyInputs, [ { - expectedPrimaryModelRef: toPrimaryModelRef(DEFAULT_MODEL), + expectedPrimaryModelRef: toPrimaryModelRef(TEST_MODEL), filePath: targetPath } ]); @@ -477,7 +477,7 @@ test("runInstallUseCase fails after writing when the Gateway is reachable but un assert.equal(state.writeCalls, 1); }); -test("runInstallUseCase uses the curated --model value without prompting for a model", async () => { +test("runInstallUseCase uses a live --model id without prompting for a model", async () => { const { dependencies, state } = createInstallHarness({ dependencies: { promptForModel: async () => { @@ -487,7 +487,7 @@ test("runInstallUseCase uses the curated --model value without prompting for a m }); await runInstallUseCase({ - modelKey: DEFAULT_MODEL_KEY, + modelId: TEST_MODEL.modelId, targetPath: "/tmp/openclaw.json" }, dependencies); @@ -500,7 +500,7 @@ test("runInstallUseCase uses the curated --model value without prompting for a m assert.deepEqual((state.writtenSettings?.agents as Record).defaults, { models: expectedAllowlist(), model: { - primary: toPrimaryModelRef(DEFAULT_MODEL) + primary: toPrimaryModelRef(TEST_MODEL) } }); }); @@ -563,8 +563,8 @@ test("runInstallUseCase surfaces a clear error when OpenClaw setup does not crea promptCalls += 1; return "gp-test-key"; }, - fetchCuratedGonkaGateModelCatalog: async () => createStaticModelCatalog(), - promptForModel: async () => DEFAULT_MODEL, + fetchGonkaGateModelCatalog: async () => createLiveModelCatalog(), + promptForModel: async () => TEST_MODEL, validateApiKey: (apiKey) => apiKey, writeSettings: async () => {} } @@ -638,7 +638,7 @@ test("runInstallUseCase stops before the model prompt when the live GonkaGate ca }); const { dependencies, state } = createInstallHarness({ dependencies: { - fetchCuratedGonkaGateModelCatalog: async () => { + fetchGonkaGateModelCatalog: async () => { state.fetchModelCatalogCalls += 1; throw failure; } @@ -659,20 +659,20 @@ test("runInstallUseCase stops before the model prompt when the live GonkaGate ca assert.equal(state.writeCalls, 0); }); -test("runInstallUseCase prompts with the curated models returned by the live GonkaGate catalog", async () => { +test("runInstallUseCase prompts with all models returned by the live GonkaGate catalog", async () => { let promptModels: string[] = []; let promptDefaultModelKey: string | undefined; const { dependencies, state } = createInstallHarness({ dependencies: { - fetchCuratedGonkaGateModelCatalog: async () => { + fetchGonkaGateModelCatalog: async () => { state.fetchModelCatalogCalls += 1; - return createStaticModelCatalog(); + return createLiveModelCatalog(); }, promptForModel: async (models, defaultModelKey) => { state.promptModelCalls += 1; promptModels = models.map((model) => model.key); promptDefaultModelKey = defaultModelKey; - return DEFAULT_MODEL; + return TEST_MODEL; } } }); @@ -681,28 +681,27 @@ test("runInstallUseCase prompts with the curated models returned by the live Gon targetPath: "/tmp/openclaw.json" }, dependencies); - assert.deepEqual(promptModels, ["qwen3-235b", "kimi-k2.6", "minimax-m2.7"]); - assert.equal(promptDefaultModelKey, DEFAULT_MODEL_KEY); + assert.deepEqual(promptModels, [TEST_MODEL.modelId, TEST_MODEL_BETA.modelId]); + assert.equal(promptDefaultModelKey, TEST_MODEL.modelId); assert.equal(state.writeCalls, 1); assert.deepEqual(((state.writtenSettings?.agents as Record).defaults as Record).model, { - primary: toPrimaryModelRef(DEFAULT_MODEL) + primary: toPrimaryModelRef(TEST_MODEL) }); }); -test("runInstallUseCase rejects --model values that are curated but absent from the live GonkaGate catalog", async () => { - const qwen = requireSupportedModel("qwen3-235b"); +test("runInstallUseCase rejects --model values absent from the live GonkaGate catalog", async () => { const { dependencies, state } = createInstallHarness({ dependencies: { - fetchCuratedGonkaGateModelCatalog: async () => { + fetchGonkaGateModelCatalog: async () => { state.fetchModelCatalogCalls += 1; - return createStaticCuratedGonkaGateModelCatalog([qwen]); + return createTestModelCatalog([{ id: TEST_MODEL_BETA.modelId, name: TEST_MODEL_BETA.displayName }]); } } }); await assert.rejects( runInstallUseCase({ - modelKey: DEFAULT_MODEL_KEY, + modelId: TEST_MODEL.modelId, targetPath: "/tmp/openclaw.json" }, dependencies), (error) => { diff --git a/test/load-settings.test.ts b/test/load-settings.test.ts index 7820834..0dfe7c8 100644 --- a/test/load-settings.test.ts +++ b/test/load-settings.test.ts @@ -1,10 +1,10 @@ import assert from "node:assert/strict"; import { writeFile } from "node:fs/promises"; import test from "node:test"; -import { SUPPORTED_MODELS, toPrimaryModelRef } from "../src/constants/models.js"; +import { toPrimaryModelRef } from "../src/constants/models.js"; import { INSTALL_ERROR_CODE, SettingsParseError, SettingsShapeError } from "../src/install/install-errors.js"; import { loadSettings } from "../src/install/load-settings.js"; -import { createTempFilePath } from "./test-helpers.js"; +import { TEST_MODEL, createTempFilePath } from "./test-helpers.js"; test("loadSettings parses JSON5 and rejects invalid JSON5 instead of overwriting it", async () => { const filePath = await createTempFilePath("openclaw-invalid-json5-"); @@ -216,14 +216,13 @@ test("loadSettings accepts configs that omit optional managed objects", async () }); }); -test("loadSettings rejects malformed managed allowlist entries for supported curated models", async () => { - for (const model of SUPPORTED_MODELS) { - const filePath = await createTempFilePath(`openclaw-invalid-allowlist-entry-${model.key}-`); - const primaryModelRef = toPrimaryModelRef(model); +test("loadSettings rejects malformed managed allowlist entries", async () => { + const filePath = await createTempFilePath("openclaw-invalid-allowlist-entry-"); + const primaryModelRef = toPrimaryModelRef(TEST_MODEL); - await writeFile( - filePath, - `{ + await writeFile( + filePath, + `{ agents: { defaults: { models: { @@ -233,20 +232,19 @@ test("loadSettings rejects malformed managed allowlist entries for supported cur }, } `, - "utf8" - ); + "utf8" + ); - await assert.rejects( - loadSettings(filePath), - (error) => { - assert.ok(error instanceof SettingsShapeError); - assert.equal(error.code, INSTALL_ERROR_CODE.settingsShapeInvalid); - assert.equal(error.fieldPath, `agents.defaults.models.${primaryModelRef}`); - assert.match(error.message, /agents\.defaults\.models\./); - return true; - } - ); - } + await assert.rejects( + loadSettings(filePath), + (error) => { + assert.ok(error instanceof SettingsShapeError); + assert.equal(error.code, INSTALL_ERROR_CODE.settingsShapeInvalid); + assert.equal(error.fieldPath, `agents.defaults.models.${primaryModelRef}`); + assert.match(error.message, /agents\.defaults\.models\./); + return true; + } + ); }); test("loadSettings returns exists=false when the config file is missing", async () => { diff --git a/test/managed-settings-access.test.ts b/test/managed-settings-access.test.ts index 724ae07..e1cdcb6 100644 --- a/test/managed-settings-access.test.ts +++ b/test/managed-settings-access.test.ts @@ -1,9 +1,9 @@ import assert from "node:assert/strict"; import test from "node:test"; -import { DEFAULT_MODEL, toPrimaryModelRef } from "../src/constants/models.js"; +import { toPrimaryModelRef } from "../src/constants/models.js"; import { readManagedSettingsView } from "../src/install/managed-settings-access.js"; import type { OpenClawConfig } from "../src/types/settings.js"; -import { createManagedConfigFixture } from "./test-helpers.js"; +import { TEST_MODEL, createManagedConfigFixture } from "./test-helpers.js"; test("readManagedSettingsView exposes managed branches without inventing optional structures", () => { const settings: OpenClawConfig = { @@ -33,7 +33,7 @@ test("readManagedSettingsView exposes managed branches without inventing optiona assert.deepEqual(view.defaults, { model: { - primary: toPrimaryModelRef(DEFAULT_MODEL) + primary: toPrimaryModelRef(TEST_MODEL) }, workspace: { root: "~/.openclaw/workspace" diff --git a/test/merge-settings.test.ts b/test/merge-settings.test.ts index c0b7921..81c6cec 100644 --- a/test/merge-settings.test.ts +++ b/test/merge-settings.test.ts @@ -1,22 +1,24 @@ import assert from "node:assert/strict"; import test from "node:test"; -import { DEFAULT_MODEL, SUPPORTED_MODELS, toPrimaryModelRef } from "../src/constants/models.js"; +import { toPrimaryModelRef } from "../src/constants/models.js"; import { SettingsShapeError } from "../src/install/install-errors.js"; -import { createStaticCuratedGonkaGateModelCatalog } from "../src/install/gonkagate-models.js"; import { mergeSettingsWithGonkaGate } from "../src/install/merge-settings.js"; +import { TEST_MODEL, TEST_MODEL_BETA, createTestModelCatalog } from "./test-helpers.js"; + +const TEST_MODEL_CATALOG = createTestModelCatalog(); function expectedAllowlist() { return Object.fromEntries( - createStaticCuratedGonkaGateModelCatalog().map((entry) => [entry.primaryModelRef, entry.allowlistEntry]) + TEST_MODEL_CATALOG.map((entry) => [entry.primaryModelRef, entry.allowlistEntry]) ); } function expectedProviderModels() { - return createStaticCuratedGonkaGateModelCatalog().map((entry) => entry.providerModel); + return TEST_MODEL_CATALOG.map((entry) => entry.providerModel); } -test("mergeSettingsWithGonkaGate adds the curated openai model catalog when the provider did not define one", () => { - const merged = mergeSettingsWithGonkaGate({}, "gp-test-key", DEFAULT_MODEL); +test("mergeSettingsWithGonkaGate adds every fetched openai model catalog entry", () => { + const merged = mergeSettingsWithGonkaGate({}, "gp-test-key", TEST_MODEL, TEST_MODEL_CATALOG); assert.deepEqual(merged.models, { providers: { @@ -30,7 +32,7 @@ test("mergeSettingsWithGonkaGate adds the curated openai model catalog when the }); }); -test("mergeSettingsWithGonkaGate merges the curated openai model catalog and unrelated provider entries", () => { +test("mergeSettingsWithGonkaGate merges fetched openai models and unrelated provider entries", () => { const existingCatalog = [ { id: "existing-model", @@ -55,7 +57,8 @@ test("mergeSettingsWithGonkaGate merges the curated openai model catalog and unr } }, "gp-test-key", - DEFAULT_MODEL + TEST_MODEL, + TEST_MODEL_CATALOG ); assert.deepEqual((merged.models as Record).providers, { @@ -78,41 +81,9 @@ test("mergeSettingsWithGonkaGate merges the curated openai model catalog and unr }); }); -test("mergeSettingsWithGonkaGate merges curated entries into existing agents.defaults.models allowlists", () => { - for (const model of SUPPORTED_MODELS) { - const merged = mergeSettingsWithGonkaGate( - { - agents: { - defaults: { - models: { - "openai/legacy-model": { - alias: "legacy" - } - } - } - } - }, - "gp-test-key", - model - ); - - assert.deepEqual((merged.agents as Record).defaults, { - model: { - primary: toPrimaryModelRef(model) - }, - models: { - "openai/legacy-model": { - alias: "legacy" - }, - ...expectedAllowlist() - } - }); - } -}); - -test("mergeSettingsWithGonkaGate creates agents.defaults.models for the curated switcher catalog", () => { - for (const model of SUPPORTED_MODELS) { - const merged = mergeSettingsWithGonkaGate({}, "gp-test-key", model); +test("mergeSettingsWithGonkaGate creates agents.defaults.models for the fetched switcher catalog", () => { + for (const model of [TEST_MODEL, TEST_MODEL_BETA]) { + const merged = mergeSettingsWithGonkaGate({}, "gp-test-key", model, TEST_MODEL_CATALOG); assert.deepEqual((merged.agents as Record).defaults, { models: expectedAllowlist(), @@ -123,15 +94,35 @@ test("mergeSettingsWithGonkaGate creates agents.defaults.models for the curated } }); -test("mergeSettingsWithGonkaGate sets the selected curated primary ref for every supported model", () => { - for (const model of SUPPORTED_MODELS) { - const merged = mergeSettingsWithGonkaGate({}, "gp-test-key", model); - const defaultModel = ( - ((merged.agents as Record).defaults as Record).model as Record - ); +test("mergeSettingsWithGonkaGate merges fetched entries into existing agents.defaults.models allowlists", () => { + const merged = mergeSettingsWithGonkaGate( + { + agents: { + defaults: { + models: { + "openai/legacy-model": { + alias: "legacy" + } + } + } + } + }, + "gp-test-key", + TEST_MODEL_BETA, + TEST_MODEL_CATALOG + ); - assert.equal(defaultModel.primary, toPrimaryModelRef(model)); - } + assert.deepEqual((merged.agents as Record).defaults, { + model: { + primary: toPrimaryModelRef(TEST_MODEL_BETA) + }, + models: { + "openai/legacy-model": { + alias: "legacy" + }, + ...expectedAllowlist() + } + }); }); test("mergeSettingsWithGonkaGate preserves unrelated top-level settings", () => { @@ -143,7 +134,8 @@ test("mergeSettingsWithGonkaGate preserves unrelated top-level settings", () => } }, "gp-test-key", - DEFAULT_MODEL + TEST_MODEL, + TEST_MODEL_CATALOG ); assert.equal(merged.theme, "solarized"); @@ -165,14 +157,15 @@ test("mergeSettingsWithGonkaGate preserves unrelated agents.defaults.model keys" } }, "gp-test-key", - DEFAULT_MODEL + TEST_MODEL, + TEST_MODEL_CATALOG ); assert.deepEqual((merged.agents as Record).defaults, { models: expectedAllowlist(), model: { fallback: "openai/legacy-model", - primary: toPrimaryModelRef(DEFAULT_MODEL), + primary: toPrimaryModelRef(TEST_MODEL), temperature: 0.2 } }); @@ -195,7 +188,8 @@ test("mergeSettingsWithGonkaGate preserves special object keys as data without m } }`), "gp-test-key", - DEFAULT_MODEL + TEST_MODEL, + TEST_MODEL_CATALOG ); const providers = (merged.models as Record).providers as Record; @@ -209,7 +203,7 @@ test("mergeSettingsWithGonkaGate preserves special object keys as data without m assert.equal((Object.getPrototypeOf(headers) as { polluted?: boolean } | null)?.polluted, undefined); }); -test("mergeSettingsWithGonkaGate rejects malformed managed allowlist entries for the selected model", () => { +test("mergeSettingsWithGonkaGate rejects malformed managed allowlist entries for fetched models", () => { assert.throws( () => mergeSettingsWithGonkaGate( @@ -217,17 +211,18 @@ test("mergeSettingsWithGonkaGate rejects malformed managed allowlist entries for agents: { defaults: { models: { - [toPrimaryModelRef(DEFAULT_MODEL)]: "not-an-object" + [toPrimaryModelRef(TEST_MODEL)]: "not-an-object" } } } }, "gp-test-key", - DEFAULT_MODEL + TEST_MODEL, + TEST_MODEL_CATALOG ), (error) => { assert.ok(error instanceof SettingsShapeError); - assert.equal(error.fieldPath, `agents.defaults.models.${toPrimaryModelRef(DEFAULT_MODEL)}`); + assert.equal(error.fieldPath, `agents.defaults.models.${toPrimaryModelRef(TEST_MODEL)}`); return true; } ); diff --git a/test/models.test.ts b/test/models.test.ts index ea2b8a8..3da9ba6 100644 --- a/test/models.test.ts +++ b/test/models.test.ts @@ -1,28 +1,21 @@ import assert from "node:assert/strict"; import test from "node:test"; -import { - DEFAULT_MODEL, - DEFAULT_MODEL_KEY, - getSupportedModelByKey, - requireSupportedModel, - SUPPORTED_MODEL_KEYS, - toPrimaryModelRef -} from "../src/constants/models.js"; +import { modelFromPrimaryRef, toManagedModelSelection, toPrimaryModelRef } from "../src/constants/models.js"; +import { TEST_MODEL } from "./test-helpers.js"; -test("curated registry includes Kimi K2.6 as the default model and MiniMax M2.7", () => { - assert.equal(DEFAULT_MODEL_KEY, "kimi-k2.6"); - assert.equal(DEFAULT_MODEL.modelId, "moonshotai/kimi-k2.6"); - assert.deepEqual(SUPPORTED_MODEL_KEYS, ["qwen3-235b", "kimi-k2.6", "minimax-m2.7"]); - - const kimi = requireSupportedModel("kimi-k2.6"); - const minimax = requireSupportedModel("minimax-m2.7"); - - assert.equal(kimi.displayName, "Kimi K2.6"); - assert.equal(kimi.modelId, "moonshotai/kimi-k2.6"); - assert.equal(toPrimaryModelRef(kimi), "openai/moonshotai/kimi-k2.6"); - assert.equal(minimax.displayName, "MiniMax M2.7"); - assert.equal(minimax.modelId, "minimaxai/minimax-m2.7"); - assert.equal(toPrimaryModelRef(minimax), "openai/minimaxai/minimax-m2.7"); - assert.equal(DEFAULT_MODEL, kimi); - assert.equal(getSupportedModelByKey("missing-model"), undefined); +test("model helpers format OpenClaw refs without a checked-in registry", () => { + assert.equal(toPrimaryModelRef(TEST_MODEL), "openai/acme/model-alpha"); + assert.deepEqual(toManagedModelSelection(TEST_MODEL), { + allowlistEntry: { + alias: "acme/model-alpha" + }, + primaryModelRef: "openai/acme/model-alpha", + selectedModel: TEST_MODEL + }); + assert.deepEqual(modelFromPrimaryRef("openai/acme/model-alpha"), { + displayName: "acme/model-alpha", + key: "acme/model-alpha", + modelId: "acme/model-alpha" + }); + assert.equal(modelFromPrimaryRef("anthropic/acme/model-alpha"), undefined); }); diff --git a/test/prompts-validate-api-key.test.ts b/test/prompts-validate-api-key.test.ts index 02de28d..840f827 100644 --- a/test/prompts-validate-api-key.test.ts +++ b/test/prompts-validate-api-key.test.ts @@ -1,62 +1,61 @@ import assert from "node:assert/strict"; import test from "node:test"; -import { DEFAULT_MODEL, DEFAULT_MODEL_KEY } from "../src/constants/models.js"; import { INSTALL_ERROR_CODE, ApiKeyValidationError, PromptError } from "../src/install/install-errors.js"; import { buildModelPromptConfig, promptForApiKey, promptForModel } from "../src/install/prompts.js"; import { validateApiKey } from "../src/install/validate-api-key.js"; -import { withPatchedTty } from "./test-helpers.js"; +import { TEST_MODEL, withPatchedTty } from "./test-helpers.js"; -test("model picker is configured with the default model for enter-to-accept flow", () => { - const promptConfig = buildModelPromptConfig([DEFAULT_MODEL], DEFAULT_MODEL_KEY); +test("model picker is configured with the first fetched model for enter-to-accept flow", () => { + const promptConfig = buildModelPromptConfig([TEST_MODEL], TEST_MODEL.modelId); - assert.equal(promptConfig.default, DEFAULT_MODEL_KEY); + assert.equal(promptConfig.default, TEST_MODEL.modelId); assert.equal(promptConfig.theme?.indexMode, "number"); }); -test("buildModelPromptConfig rejects an empty curated model registry", () => { +test("buildModelPromptConfig rejects an empty model catalog", () => { assert.throws( - () => buildModelPromptConfig([], DEFAULT_MODEL_KEY), + () => buildModelPromptConfig([], TEST_MODEL.modelId), (error) => { assert.ok(error instanceof PromptError); assert.equal(error.code, INSTALL_ERROR_CODE.promptFailed); - assert.equal(error.kind, "no_supported_models"); + assert.equal(error.kind, "no_models"); return true; } ); }); -test("buildModelPromptConfig rejects default model keys that are not present in the registry", () => { +test("buildModelPromptConfig rejects default model ids that are not present in the catalog", () => { assert.throws( - () => buildModelPromptConfig([DEFAULT_MODEL], "missing-model" as typeof DEFAULT_MODEL_KEY), + () => buildModelPromptConfig([TEST_MODEL], "missing-model"), (error) => { assert.ok(error instanceof PromptError); - assert.equal(error.kind, "model_registry_mismatch"); + assert.equal(error.kind, "model_catalog_mismatch"); return true; } ); }); -test("promptForModel returns the default model when the prompt resolves to the default key", async () => { +test("promptForModel returns the default model when the prompt resolves to the default id", async () => { const selectedModel = await promptForModel( - [DEFAULT_MODEL], - DEFAULT_MODEL_KEY, + [TEST_MODEL], + TEST_MODEL.modelId, async (config) => config.default ); - assert.equal(selectedModel.key, DEFAULT_MODEL_KEY); - assert.equal(selectedModel.modelId, DEFAULT_MODEL.modelId); + assert.equal(selectedModel.key, TEST_MODEL.modelId); + assert.equal(selectedModel.modelId, TEST_MODEL.modelId); }); -test("promptForModel rejects model keys that are not in the curated registry", async () => { +test("promptForModel rejects model ids that are not in the fetched catalog", async () => { await assert.rejects( promptForModel( - [DEFAULT_MODEL], - DEFAULT_MODEL_KEY, - async () => "missing-model" as typeof DEFAULT_MODEL_KEY + [TEST_MODEL], + TEST_MODEL.modelId, + async () => "missing-model" ), (error) => { assert.ok(error instanceof PromptError); - assert.equal(error.kind, "model_registry_mismatch"); + assert.equal(error.kind, "model_catalog_mismatch"); return true; } ); @@ -67,7 +66,7 @@ test("promptForModel maps prompt cancellation errors to a friendly message", asy for (const errorName of cancellationErrors) { await assert.rejects( - promptForModel([DEFAULT_MODEL], DEFAULT_MODEL_KEY, async () => { + promptForModel([TEST_MODEL], TEST_MODEL.modelId, async () => { const error = new Error("cancelled"); error.name = errorName; throw error; diff --git a/test/test-helpers.ts b/test/test-helpers.ts index ea712e6..78d5fe2 100644 --- a/test/test-helpers.ts +++ b/test/test-helpers.ts @@ -3,10 +3,37 @@ import { tmpdir } from "node:os"; import path from "node:path"; import process from "node:process"; import { GONKAGATE_OPENAI_API, GONKAGATE_OPENAI_BASE_URL } from "../src/constants/gateway.js"; -import { DEFAULT_MODEL, toPrimaryModelRef, type SupportedModel } from "../src/constants/models.js"; -import { createStaticCuratedGonkaGateModelCatalog } from "../src/install/gonkagate-models.js"; +import { toPrimaryModelRef, type GonkaGateModel } from "../src/constants/models.js"; +import { createGonkaGateModelCatalog, type LiveGonkaGateModel } from "../src/install/gonkagate-models.js"; import type { OpenClawConfig } from "../src/types/settings.js"; +export const TEST_LIVE_MODELS: readonly LiveGonkaGateModel[] = [ + { + id: "acme/model-alpha", + name: "Acme Model Alpha" + }, + { + id: "globex/model-beta", + name: "Globex Model Beta" + } +]; + +export const TEST_MODEL: GonkaGateModel = { + displayName: "Acme Model Alpha", + key: "acme/model-alpha", + modelId: "acme/model-alpha" +}; + +export const TEST_MODEL_BETA: GonkaGateModel = { + displayName: "Globex Model Beta", + key: "globex/model-beta", + modelId: "globex/model-beta" +}; + +export function createTestModelCatalog(models: readonly LiveGonkaGateModel[] = TEST_LIVE_MODELS) { + return createGonkaGateModelCatalog(models); +} + export async function createTempDirectory(prefix: string): Promise { return mkdtemp(path.join(tmpdir(), prefix)); } @@ -70,15 +97,15 @@ interface ManagedConfigFixtureOptions { includeOpenAiModels?: boolean; openaiProvider?: Record; primaryModelRef?: string; - selectedModel?: SupportedModel; + selectedModel?: GonkaGateModel; } export function createManagedConfigFixture(options: ManagedConfigFixtureOptions = {}): OpenClawConfig { - const selectedModel = options.selectedModel ?? DEFAULT_MODEL; + const selectedModel = options.selectedModel ?? TEST_MODEL; const primaryModelRef = options.primaryModelRef ?? toPrimaryModelRef(selectedModel); const defaults = asRecord(options.defaults); const defaultModel = asRecord(defaults.model); - const modelCatalog = createStaticCuratedGonkaGateModelCatalog(); + const modelCatalog = createTestModelCatalog(); const allowlist = options.allowlist ?? Object.fromEntries( modelCatalog.map((entry) => [entry.primaryModelRef, entry.allowlistEntry]) ); diff --git a/test/verify-runtime.test.ts b/test/verify-runtime.test.ts index feacc92..7fa6994 100644 --- a/test/verify-runtime.test.ts +++ b/test/verify-runtime.test.ts @@ -1,6 +1,7 @@ import assert from "node:assert/strict"; import test from "node:test"; -import { toPrimaryModelRef, DEFAULT_MODEL } from "../src/constants/models.js"; +import { toPrimaryModelRef } from "../src/constants/models.js"; +import { TEST_MODEL } from "./test-helpers.js"; import { INSTALL_ERROR_CODE, RuntimeVerificationError, @@ -48,7 +49,7 @@ function createProbeClient(results: readonly StubCommandResult[]) { } test("createOpenClawClient uses the canonical OpenClaw probe commands with piped stdio", () => { - const expectedPrimaryModelRef = toPrimaryModelRef(DEFAULT_MODEL); + const expectedPrimaryModelRef = toPrimaryModelRef(TEST_MODEL); const calls: Array<{ args: string[]; command: string; @@ -116,7 +117,7 @@ test("createOpenClawClient uses the canonical OpenClaw probe commands with piped }); test("verifyOpenClawRuntime accepts a healthy Gateway, health snapshot, and resolved model", () => { - const expectedPrimaryModelRef = toPrimaryModelRef(DEFAULT_MODEL); + const expectedPrimaryModelRef = toPrimaryModelRef(TEST_MODEL); const result = verifyOpenClawRuntime( "/tmp/openclaw.json", @@ -163,7 +164,7 @@ test("createOpenClawClient keeps parsed-output metadata on non-zero probe exits" }); test("createOpenClawClient parses a single resolved model line and tolerates non-model log noise", () => { - const expectedPrimaryModelRef = toPrimaryModelRef(DEFAULT_MODEL); + const expectedPrimaryModelRef = toPrimaryModelRef(TEST_MODEL); const parsedResult = createProbeClient([ { status: 0, stdout: `${expectedPrimaryModelRef}\n` } ]).probeResolvedPrimaryModel(); @@ -193,7 +194,7 @@ test("createOpenClawClient parses a single resolved model line and tolerates non }); test("createOpenClawClient rejects resolved-model output that contains multiple model refs", () => { - const expectedPrimaryModelRef = toPrimaryModelRef(DEFAULT_MODEL); + const expectedPrimaryModelRef = toPrimaryModelRef(TEST_MODEL); const unparsedResult = createProbeClient([ { status: 0, stdout: `${expectedPrimaryModelRef}\nopenai/other-model\n` } ]).probeResolvedPrimaryModel(); @@ -210,7 +211,7 @@ test("createOpenClawClient rejects resolved-model output that contains multiple test("verifyOpenClawRuntime distinguishes Gateway probe command failures from gateway-unavailable reports", () => { const result = verifyOpenClawRuntime( "/tmp/openclaw.json", - toPrimaryModelRef(DEFAULT_MODEL), + toPrimaryModelRef(TEST_MODEL), createProbeClient([ { status: 1, stderr: "rpc failed" } ]) @@ -229,7 +230,7 @@ test("verifyOpenClawRuntime distinguishes Gateway probe command failures from ga test("verifyOpenClawRuntime rejects malformed gateway status output even when the command succeeds", () => { const result = verifyOpenClawRuntime( "/tmp/openclaw.json", - toPrimaryModelRef(DEFAULT_MODEL), + toPrimaryModelRef(TEST_MODEL), createProbeClient([ { status: 0, stdout: "not json" } ]) @@ -246,7 +247,7 @@ test("verifyOpenClawRuntime rejects malformed gateway status output even when th test("verifyOpenClawRuntime rejects malformed health output even when the command succeeds", () => { const result = verifyOpenClawRuntime( "/tmp/openclaw.json", - toPrimaryModelRef(DEFAULT_MODEL), + toPrimaryModelRef(TEST_MODEL), createProbeClient([ { status: 0, stdout: '{"rpc":{"ok":true}}' }, { status: 0, stdout: "[]" } @@ -264,7 +265,7 @@ test("verifyOpenClawRuntime rejects malformed health output even when the comman test("verifyOpenClawRuntime rejects gateway status reports whose rpc.ok flag is false", () => { const result = verifyOpenClawRuntime( "/tmp/openclaw.json", - toPrimaryModelRef(DEFAULT_MODEL), + toPrimaryModelRef(TEST_MODEL), createProbeClient([ { status: 0, stdout: '{"rpc":{"ok":false}}' } ]) @@ -281,7 +282,7 @@ test("verifyOpenClawRuntime rejects gateway status reports whose rpc.ok flag is test("verifyOpenClawRuntime treats structured non-zero gateway reports with rpc.ok=false as gateway unavailable", () => { const result = verifyOpenClawRuntime( "/tmp/openclaw.json", - toPrimaryModelRef(DEFAULT_MODEL), + toPrimaryModelRef(TEST_MODEL), createProbeClient([ { status: 1, stdout: '{"rpc":{"ok":false}}' } ]) @@ -298,7 +299,7 @@ test("verifyOpenClawRuntime treats structured non-zero gateway reports with rpc. test("verifyOpenClawRuntime rejects unhealthy health snapshots", () => { const result = verifyOpenClawRuntime( "/tmp/openclaw.json", - toPrimaryModelRef(DEFAULT_MODEL), + toPrimaryModelRef(TEST_MODEL), createProbeClient([ { status: 0, stdout: '{"rpc":{"ok":true}}' }, { status: 0, stdout: '{"ok":false}' } @@ -316,7 +317,7 @@ test("verifyOpenClawRuntime rejects unhealthy health snapshots", () => { test("verifyOpenClawRuntime rejects empty resolved-model output", () => { const result = verifyOpenClawRuntime( "/tmp/openclaw.json", - toPrimaryModelRef(DEFAULT_MODEL), + toPrimaryModelRef(TEST_MODEL), createProbeClient([ { status: 0, stdout: '{"rpc":{"ok":true}}' }, { status: 0, stdout: '{"ok":true}' }, @@ -333,7 +334,7 @@ test("verifyOpenClawRuntime rejects empty resolved-model output", () => { }); test("verifyOpenClawRuntime rejects ambiguous resolved-model output as unexpected", () => { - const expectedPrimaryModelRef = toPrimaryModelRef(DEFAULT_MODEL); + const expectedPrimaryModelRef = toPrimaryModelRef(TEST_MODEL); const result = verifyOpenClawRuntime( "/tmp/openclaw.json", expectedPrimaryModelRef, @@ -355,7 +356,7 @@ test("verifyOpenClawRuntime rejects ambiguous resolved-model output as unexpecte test("verifyOpenClawRuntime rejects mismatched resolved models", () => { const result = verifyOpenClawRuntime( "/tmp/openclaw.json", - toPrimaryModelRef(DEFAULT_MODEL), + toPrimaryModelRef(TEST_MODEL), createProbeClient([ { status: 0, stdout: '{"rpc":{"ok":true}}' }, { status: 0, stdout: '{"ok":true}' }, @@ -374,7 +375,7 @@ test("verifyOpenClawRuntime rejects mismatched resolved models", () => { test("verifyOpenClawRuntimeForInstall tolerates gateway-unavailable results and maps the next command", () => { const result = verifyOpenClawRuntimeForInstall( "/tmp/openclaw.json", - toPrimaryModelRef(DEFAULT_MODEL), + toPrimaryModelRef(TEST_MODEL), createProbeClient([ { status: 1, stdout: '{"rpc":{"ok":false}}' } ]) @@ -391,7 +392,7 @@ test("verifyOpenClawRuntimeForInstall keeps malformed gateway status output stri () => verifyOpenClawRuntimeForInstall( "/tmp/openclaw.json", - toPrimaryModelRef(DEFAULT_MODEL), + toPrimaryModelRef(TEST_MODEL), createProbeClient([ { status: 0, stdout: "not json" } ]) @@ -414,7 +415,7 @@ test("verifyOpenClawRuntimeForInstall rethrows strict runtime failures for insta () => verifyOpenClawRuntimeForInstall( "/tmp/openclaw.json", - toPrimaryModelRef(DEFAULT_MODEL), + toPrimaryModelRef(TEST_MODEL), createProbeClient([ { status: 0, stdout: '{"rpc":{"ok":true}}' }, { status: 0, stdout: '{"ok":false}' } @@ -438,7 +439,7 @@ test("verifyOpenClawRuntimeForVerify keeps probe command failures strict", () => () => verifyOpenClawRuntimeForVerify( "/tmp/openclaw.json", - toPrimaryModelRef(DEFAULT_MODEL), + toPrimaryModelRef(TEST_MODEL), createProbeClient([ { status: 1, stderr: "rpc failed" } ]) @@ -458,7 +459,7 @@ test("verifyOpenClawRuntimeForVerify keeps probe command failures strict", () => }); test("verifyOpenClawRuntimeForVerify preserves successful runtime results unchanged", () => { - const expectedPrimaryModelRef = toPrimaryModelRef(DEFAULT_MODEL); + const expectedPrimaryModelRef = toPrimaryModelRef(TEST_MODEL); const result = verifyOpenClawRuntimeForVerify( "/tmp/openclaw.json", expectedPrimaryModelRef, @@ -505,7 +506,7 @@ test("verifyOpenClawRuntimeForInstall rethrows every strict failure kind except ]); assert.throws( - () => verifyOpenClawRuntimeForInstall("/tmp/openclaw.json", toPrimaryModelRef(DEFAULT_MODEL), runner), + () => verifyOpenClawRuntimeForInstall("/tmp/openclaw.json", toPrimaryModelRef(TEST_MODEL), runner), (error) => { assert.ok(error instanceof RuntimeVerificationError); assert.equal(error.code, INSTALL_ERROR_CODE.runtimeVerificationFailed); diff --git a/test/verify-settings.test.ts b/test/verify-settings.test.ts index f15ab89..a9b66fb 100644 --- a/test/verify-settings.test.ts +++ b/test/verify-settings.test.ts @@ -1,38 +1,45 @@ import assert from "node:assert/strict"; import { chmod, writeFile } from "node:fs/promises"; import test from "node:test"; -import { DEFAULT_MODEL, SUPPORTED_MODELS, toPrimaryModelRef } from "../src/constants/models.js"; +import { toPrimaryModelRef } from "../src/constants/models.js"; import { formatUnixMode } from "../src/install/file-permissions.js"; import { INSTALL_ERROR_CODE, SettingsVerificationError } from "../src/install/install-errors.js"; import { loadSettings } from "../src/install/load-settings.js"; import { mergeSettingsWithGonkaGate } from "../src/install/merge-settings.js"; import { verifySettings } from "../src/install/verify-settings.js"; import { writeSettings } from "../src/install/write-settings.js"; -import { createManagedConfigFixture, createTempFilePath } from "./test-helpers.js"; +import { + TEST_MODEL, + TEST_MODEL_BETA, + createManagedConfigFixture, + createTempFilePath, + createTestModelCatalog +} from "./test-helpers.js"; + +const TEST_MODEL_CATALOG = createTestModelCatalog(); function expectedAllowlist() { return Object.fromEntries( - SUPPORTED_MODELS.map((model) => [toPrimaryModelRef(model), { alias: model.key }]) + TEST_MODEL_CATALOG.map((entry) => [entry.primaryModelRef, entry.allowlistEntry]) ); } test("verifySettings accepts the managed GonkaGate provider config with owner-only permissions", async () => { - for (const model of SUPPORTED_MODELS) { - const filePath = await createManagedConfigFile(`openclaw-verify-success-${model.key}-`, 0o600); + const filePath = await createManagedConfigFile("openclaw-verify-success-", 0o600); - const result = await verifySettings(filePath, createManagedConfigFixture({ - includeAllowlist: true, - selectedModel: model - })); + const result = await verifySettings(filePath, createManagedConfigFixture({ + includeAllowlist: true, + selectedModel: TEST_MODEL_BETA + })); - assert.equal(result.selectedModel.key, model.key); - assert.equal(result.configMode, 0o600); - } + assert.equal(result.selectedModel.modelId, TEST_MODEL_BETA.modelId); + assert.equal(result.selectedModel.displayName, TEST_MODEL_BETA.displayName); + assert.equal(result.configMode, 0o600); }); -test("merged configs roundtrip through write, load, and verify for every supported model", async () => { - for (const model of SUPPORTED_MODELS) { - const filePath = await createTempFilePath(`openclaw-verify-roundtrip-${model.key}-`); +test("merged configs roundtrip through write, load, and verify for fetched models", async () => { + for (const model of [TEST_MODEL, TEST_MODEL_BETA]) { + const filePath = await createTempFilePath(`openclaw-verify-roundtrip-${model.key.replaceAll("/", "-")}-`); const merged = mergeSettingsWithGonkaGate( { gateway: { @@ -45,7 +52,8 @@ test("merged configs roundtrip through write, load, and verify for every support } }, "gp-test-key", - model + model, + TEST_MODEL_CATALOG ); await writeSettings(filePath, merged); @@ -59,63 +67,62 @@ test("merged configs roundtrip through write, load, and verify for every support const result = await verifySettings(filePath, loaded.settings); - assert.equal(result.selectedModel.key, model.key); + assert.equal(result.selectedModel.modelId, model.modelId); assert.equal(result.configMode, 0o600); } }); -test("merged configs with an existing allowlist roundtrip through write, load, and verify for every supported model", async () => { - for (const model of SUPPORTED_MODELS) { - const filePath = await createTempFilePath(`openclaw-verify-allowlist-roundtrip-${model.key}-`); - const merged = mergeSettingsWithGonkaGate( - { - agents: { - defaults: { - models: { - "openai/legacy-model": { - alias: "legacy" - } +test("merged configs with an existing allowlist roundtrip through write, load, and verify", async () => { + const filePath = await createTempFilePath("openclaw-verify-allowlist-roundtrip-"); + const merged = mergeSettingsWithGonkaGate( + { + agents: { + defaults: { + models: { + "openai/legacy-model": { + alias: "legacy" } } - }, - models: { - providers: { - openai: { - headers: { - "x-extra-header": "keep-me" - } + } + }, + models: { + providers: { + openai: { + headers: { + "x-extra-header": "keep-me" } } } - }, - "gp-test-key", - model - ); + } + }, + "gp-test-key", + TEST_MODEL_BETA, + TEST_MODEL_CATALOG + ); - await writeSettings(filePath, merged); + await writeSettings(filePath, merged); - const loaded = await loadSettings(filePath); + const loaded = await loadSettings(filePath); - assert.equal(loaded.kind, "loaded"); - if (loaded.kind !== "loaded") { - assert.fail("Expected loadSettings to return the written settings"); - } + assert.equal(loaded.kind, "loaded"); + if (loaded.kind !== "loaded") { + assert.fail("Expected loadSettings to return the written settings"); + } - const result = await verifySettings(filePath, loaded.settings); + const result = await verifySettings(filePath, loaded.settings); - assert.equal(result.selectedModel.key, model.key); + assert.equal(result.selectedModel.modelId, TEST_MODEL_BETA.modelId); - const defaults = ((loaded.settings.agents as Record).defaults as Record); - const allowlist = defaults.models as Record; - const provider = (((loaded.settings.models as Record).providers as Record).openai as Record); + const defaults = ((loaded.settings.agents as Record).defaults as Record); + const allowlist = defaults.models as Record; + const provider = (((loaded.settings.models as Record).providers as Record).openai as Record); - assert.deepEqual(allowlist[toPrimaryModelRef(model)], { - alias: model.key - }); - assert.deepEqual(provider.headers, { - "x-extra-header": "keep-me" - }); - } + assert.deepEqual(allowlist[toPrimaryModelRef(TEST_MODEL_BETA)], { + alias: TEST_MODEL_BETA.modelId + }); + assert.deepEqual(provider.headers, { + "x-extra-header": "keep-me" + }); }); test("verifySettings rejects configs that point OpenClaw at the wrong base URL", async () => { @@ -200,24 +207,24 @@ test("verifySettings rejects saved API keys with leading or trailing whitespace" ); }); -test("verifySettings rejects unsupported primary model refs", async () => { +test("verifySettings rejects primary model refs that are not openai model refs", async () => { const filePath = await createManagedConfigFile("openclaw-verify-model-ref-", 0o600); await assert.rejects( verifySettings(filePath, createManagedConfigFixture({ - primaryModelRef: "openai/not-supported" + primaryModelRef: "anthropic/acme/model-alpha" })), (error) => { assert.ok(error instanceof SettingsVerificationError); assert.equal(error.kind, "mismatched_managed_value"); assert.equal(error.fieldPath, "agents.defaults.model.primary"); - assert.equal(error.actual, "openai/not-supported"); + assert.equal(error.actual, "anthropic/acme/model-alpha"); return true; } ); }); -test("verifySettings rejects missing allowlist entries when agents.defaults.models is present", async () => { +test("verifySettings rejects missing allowlist entry for the selected model", async () => { const filePath = await createManagedConfigFile("openclaw-verify-allowlist-", 0o600); await assert.rejects( @@ -234,7 +241,7 @@ test("verifySettings rejects missing allowlist entries when agents.defaults.mode ); }); -test("verifySettings rejects configs without the curated model switcher allowlist", async () => { +test("verifySettings rejects configs without the model switcher allowlist", async () => { const filePath = await createManagedConfigFile("openclaw-verify-missing-allowlist-", 0o600); await assert.rejects( @@ -250,38 +257,41 @@ test("verifySettings rejects configs without the curated model switcher allowlis ); }); -test("verifySettings rejects mismatched allowlist aliases when agents.defaults.models is present", async () => { - for (const model of SUPPORTED_MODELS) { - const filePath = await createManagedConfigFile(`openclaw-verify-alias-${model.key}-`, 0o600); - const allowlist = expectedAllowlist(); - allowlist[toPrimaryModelRef(model)] = { - alias: "wrong-alias" - }; - - await assert.rejects( - verifySettings(filePath, createManagedConfigFixture({ - allowlist, - includeAllowlist: true, - selectedModel: model - })), - (error) => { - assert.ok(error instanceof SettingsVerificationError); - assert.equal(error.kind, "mismatched_allowlist_alias"); - assert.match(error.fieldPath ?? "", /alias/); - assert.equal(error.actual, "wrong-alias"); - return true; - } - ); - } +test("verifySettings rejects mismatched allowlist aliases for the selected model", async () => { + const filePath = await createManagedConfigFile("openclaw-verify-alias-", 0o600); + const allowlist = expectedAllowlist(); + allowlist[toPrimaryModelRef(TEST_MODEL)] = { + alias: "wrong-alias" + }; + + await assert.rejects( + verifySettings(filePath, createManagedConfigFixture({ + allowlist, + includeAllowlist: true, + selectedModel: TEST_MODEL + })), + (error) => { + assert.ok(error instanceof SettingsVerificationError); + assert.equal(error.kind, "mismatched_allowlist_alias"); + assert.match(error.fieldPath ?? "", /alias/); + assert.equal(error.actual, "wrong-alias"); + return true; + } + ); }); -test("verifySettings rejects configs whose openai model catalog omits a curated model id", async () => { +test("verifySettings rejects configs whose openai model catalog omits the selected model id", async () => { const filePath = await createManagedConfigFile("openclaw-verify-provider-model-entry-", 0o600); await assert.rejects( verifySettings(filePath, createManagedConfigFixture({ openaiProvider: { - models: [] + models: [ + { + id: "other/model", + name: "Other Model" + } + ] } })), (error) => { diff --git a/test/verify-use-case.test.ts b/test/verify-use-case.test.ts index 1aa732c..9db1f37 100644 --- a/test/verify-use-case.test.ts +++ b/test/verify-use-case.test.ts @@ -1,12 +1,12 @@ import assert from "node:assert/strict"; import { chmod, readFile, writeFile } from "node:fs/promises"; import test from "node:test"; -import { DEFAULT_MODEL, toPrimaryModelRef } from "../src/constants/models.js"; +import { toPrimaryModelRef } from "../src/constants/models.js"; import { runVerifyUseCase, type VerifyUseCaseDependencies } from "../src/install/verify-use-case.js"; -import { createManagedConfigFixture, createTempFilePath } from "./test-helpers.js"; +import { TEST_MODEL, createManagedConfigFixture, createTempFilePath } from "./test-helpers.js"; interface RuntimeVerifyInput { expectedPrimaryModelRef: string; @@ -71,7 +71,7 @@ function createVerifyHarness( recordStep(state, "verifyRuntime"); return { kind: "healthy", - resolvedPrimaryModelRef: toPrimaryModelRef(DEFAULT_MODEL) + resolvedPrimaryModelRef: toPrimaryModelRef(TEST_MODEL) }; }, ...overrides.openClaw @@ -94,7 +94,7 @@ function createVerifyHarness( recordStep(state, "verify"); return { configMode: 0o600, - selectedModel: DEFAULT_MODEL + selectedModel: TEST_MODEL }; }, ...overrides.dependencies, @@ -135,7 +135,7 @@ test("runVerifyUseCase uses a resolved legacy target path for validation and run assert.deepEqual(state.verifyPaths, [targetPath]); assert.deepEqual(state.runtimeVerifyInputs, [ { - expectedPrimaryModelRef: toPrimaryModelRef(DEFAULT_MODEL), + expectedPrimaryModelRef: toPrimaryModelRef(TEST_MODEL), filePath: targetPath } ]); @@ -206,11 +206,11 @@ test("runVerifyUseCase succeeds against a real temp config without mutating it", agents: { defaults: { model: { - primary: "${toPrimaryModelRef(DEFAULT_MODEL)}", + primary: "${toPrimaryModelRef(TEST_MODEL)}", }, models: { - "${toPrimaryModelRef(DEFAULT_MODEL)}": { - alias: "${DEFAULT_MODEL.key}", + "${toPrimaryModelRef(TEST_MODEL)}": { + alias: "${TEST_MODEL.key}", }, }, }, @@ -237,12 +237,12 @@ test("runVerifyUseCase succeeds against a real temp config without mutating it", validateConfig: () => {}, verifyRuntimeForVerify: () => ({ kind: "healthy", - resolvedPrimaryModelRef: toPrimaryModelRef(DEFAULT_MODEL) + resolvedPrimaryModelRef: toPrimaryModelRef(TEST_MODEL) }) }, verifySettings: async () => ({ configMode: 0o600, - selectedModel: DEFAULT_MODEL + selectedModel: TEST_MODEL }) } );