diff --git a/AGENTS.md b/AGENTS.md index 1796957..948ad5e 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -19,6 +19,8 @@ Current honest state: - this repository contains a TypeScript CLI that writes OpenHuman's native GonkaGate custom-provider config shape +- the CLI fetches `GET https://api.gonkagate.com/v1/models` with the user's API + key and uses that live response as the runtime model source of truth - the CLI preserves unrelated TOML config, creates one timestamped backup when replacing an existing config file, and verifies the written shape - the CLI accepts secrets only through hidden prompt, `GONKAGATE_API_KEY`, or @@ -37,14 +39,15 @@ security flow changes, update this file immediately. - the stable GonkaGate provider id is `gonkagate` - the canonical GonkaGate base URL is `https://api.gonkagate.com/v1` - the current transport target is `/v1/chat/completions` +- the runtime model catalog source is authenticated `/v1/models` - OpenHuman configuration is TOML-based and must preserve unrelated user config - `gp-...` secrets must never be printed - secrets must not be accepted through a plain `--api-key` flag - safe secret inputs are hidden prompt, `GONKAGATE_API_KEY`, and `--api-key-stdin` -- setup success is based on local config verification and an optional direct - GonkaGate smoke check when a key is available, with OpenHuman sign-in reported - as a separate runtime gate +- setup success is based on live model-catalog fetch, local config verification, + and a direct GonkaGate smoke check, with OpenHuman sign-in reported as a + separate runtime gate ## OpenHuman Integration Notes @@ -52,7 +55,7 @@ OpenHuman already has an OpenAI-compatible provider path, but the setup utility must write the current native custom-provider shape: - `cloud_providers` entry with slug `gonkagate` -- workload provider strings such as `gonkagate:moonshotai/kimi-k2.6` +- workload provider strings such as `gonkagate:` - provider credentials under `provider:gonkagate`, profile `default`, entered through OpenHuman Settings -> AI unless a future supported OpenHuman RPC/API write path is added @@ -66,8 +69,8 @@ The setup utility must still account for OpenHuman's internal tier model names: - `fast-v1` GonkaGate expects concrete model ids. The CLI routes workloads to concrete -GonkaGate model ids and reports OpenHuman's active-session gate for custom -providers when the user is signed out. +GonkaGate model ids returned by `/v1/models` and reports OpenHuman's +active-session gate for custom providers when the user is signed out. ## Change Discipline diff --git a/CHANGELOG.md b/CHANGELOG.md index f253e05..427eb51 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,10 @@ # Changelog +## Unreleased + +- Fetch GonkaGate models dynamically from authenticated `/v1/models` and remove + runtime reliance on a hardcoded catalog. + ## 0.1.0 - Initial TypeScript CLI scaffold for `@gonkagate/openhuman-setup`. diff --git a/README.md b/README.md index 92dad53..54d04bd 100644 --- a/README.md +++ b/README.md @@ -26,9 +26,9 @@ GonkaGate configured as a native OpenAI-compatible custom provider without hand-editing OpenHuman TOML config. The CLI writes OpenHuman's native `cloud_providers` shape, routes OpenHuman -agent workloads to curated GonkaGate model IDs, preserves unrelated config, -creates a timestamped backup before replacing an existing config file, and -verifies the written result. +agent workloads to live GonkaGate model IDs from `/v1/models`, preserves +unrelated config, creates a timestamped backup before replacing an existing +config file, and verifies the written result. Current OpenHuman credential storage is not a safe standalone JSON write target. OpenHuman may store provider secrets in the OS keychain or encrypted JSON @@ -54,13 +54,16 @@ You need: - Detects the effective OpenHuman `config.toml`, including `OPENHUMAN_WORKSPACE`, active user, legacy active workspace, staging root, and pre-login local config. +- Fetches `GET https://api.gonkagate.com/v1/models` with the user's GonkaGate + API key and treats that response as the model source of truth. - Adds one GonkaGate `cloud_providers` entry with slug `gonkagate`. - Writes workload provider strings for chat, reasoning, agentic, coding, and memory/summarization paths. - Preserves unrelated TOML values and unrelated cloud providers. - Creates one timestamped backup before replacing an existing config file. - Verifies the local config shape after writing. -- Runs a direct GonkaGate `/v1/chat/completions` smoke when a key is available. +- Runs a direct GonkaGate `/v1/chat/completions` smoke with the selected live + model. - Reports OpenHuman sign-in and credential state separately. ## Target OpenHuman Contract @@ -72,8 +75,7 @@ You need: - OpenHuman provider shape: `cloud_providers` entry plus workload provider strings - OpenHuman credential key: `provider:gonkagate`, profile `default` -- Recommended model: `moonshotai/kimi-k2.6` -- Additional validated model: `qwen/qwen3-235b-a22b-instruct-2507-fp8` +- Model catalog: authenticated `GET /v1/models` with the user's API key ## Safe Secret Handling @@ -104,19 +106,16 @@ printf '%s' "$GONKAGATE_API_KEY" | npx @gonkagate/openhuman-setup --api-key-stdi ## Model Overrides -Defaults: - -- reasoning, agentic, coding: `gonkagate:moonshotai/kimi-k2.6` -- chat, memory/summarization: `gonkagate:qwen/qwen3-235b-a22b-instruct-2507-fp8` - -Use curated model keys only: +By default, the CLI uses the first model returned by GonkaGate `/v1/models` for +all OpenHuman workloads. Override values must be IDs returned by that live +response: ```bash npx @gonkagate/openhuman-setup \ - --reasoning-model kimi-k2.6 \ - --agentic-model kimi-k2.6 \ - --coding-model kimi-k2.6 \ - --summarization-model qwen3-235b-a22b-instruct-2507-fp8 + --reasoning-model provider/model-id \ + --agentic-model provider/model-id \ + --coding-model provider/model-id \ + --summarization-model provider/model-id ``` ## What This Tool Does Not Do @@ -124,7 +123,7 @@ npx @gonkagate/openhuman-setup \ - It does not install OpenHuman. - It does not write the old `api_url` + `api_key` + `model_routes` setup shape. - It does not accept arbitrary custom base URLs. -- It does not accept arbitrary raw model IDs. +- It does not use repository-hardcoded GonkaGate model catalogs. - It does not print or persist `gp-...` secrets outside approved OpenHuman credential handling. - It does not directly mutate OpenHuman `auth-profiles.json`. @@ -148,7 +147,7 @@ The CLI can be exercised against a temporary OpenHuman workspace with: ```bash npm run build -OPENHUMAN_WORKSPACE="$(mktemp -d)" node bin/gonkagate-openhuman.js --json --yes +GONKAGATE_API_KEY=gp-... OPENHUMAN_WORKSPACE="$(mktemp -d)" node bin/gonkagate-openhuman.js --json --yes ``` The product requirements for the implementation live in diff --git a/docs/how-it-works.md b/docs/how-it-works.md index c38c48e..549e080 100644 --- a/docs/how-it-works.md +++ b/docs/how-it-works.md @@ -4,10 +4,11 @@ The utility: 1. detect the active OpenHuman configuration location 2. collect a GonkaGate API key through a safe input path -3. preserve unrelated OpenHuman TOML config -4. configure GonkaGate as an OpenHuman `cloud_providers` entry -5. write workload provider strings for OpenHuman task tiers -6. verify the effective setup with a real GonkaGate/OpenHuman smoke check +3. fetch `GET https://api.gonkagate.com/v1/models` with Bearer auth +4. preserve unrelated OpenHuman TOML config +5. configure GonkaGate as an OpenHuman `cloud_providers` entry +6. write workload provider strings using only fetched live model ids +7. verify the effective setup with a real GonkaGate/OpenHuman smoke check Direct OpenHuman credential-store mutation is intentionally skipped. Current OpenHuman writes provider secrets through runtime keychain or encrypted JSON @@ -25,13 +26,16 @@ label = "GonkaGate" endpoint = "https://api.gonkagate.com/v1" auth_style = "bearer" -chat_provider = "gonkagate:qwen/qwen3-235b-a22b-instruct-2507-fp8" -reasoning_provider = "gonkagate:moonshotai/kimi-k2.6" -agentic_provider = "gonkagate:moonshotai/kimi-k2.6" -coding_provider = "gonkagate:moonshotai/kimi-k2.6" -memory_provider = "gonkagate:qwen/qwen3-235b-a22b-instruct-2507-fp8" +chat_provider = "gonkagate:" +reasoning_provider = "gonkagate:" +agentic_provider = "gonkagate:" +coding_provider = "gonkagate:" +memory_provider = "gonkagate:" ``` +The concrete model id comes from the authenticated `/v1/models` response, not a +repository catalog. + The implementation must also handle OpenHuman tier model names such as `reasoning-v1`, `agentic-v1`, `coding-v1`, `summarization-v1`, and `fast-v1`. @@ -42,5 +46,5 @@ JSON output includes separate machine-readable states for: - config write and backup path - credential presence or manual key action - local config verification -- direct GonkaGate smoke check when a key is available +- direct GonkaGate smoke check - OpenHuman app-session presence diff --git a/docs/security.md b/docs/security.md index b9a9c32..c7b220d 100644 --- a/docs/security.md +++ b/docs/security.md @@ -10,6 +10,7 @@ close to the existing GonkaGate setup tools. - the CLI must not accept a plain `--api-key` flag - support safe inputs only: hidden prompt, `GONKAGATE_API_KEY`, and `--api-key-stdin` +- use the key to fetch the live GonkaGate `/v1/models` catalog with Bearer auth - preserve unrelated OpenHuman configuration - create backups before managed rewrites - redact secret-bearing diagnostics and error messages diff --git a/docs/specs/openhuman-setup-prd/fact-check.md b/docs/specs/openhuman-setup-prd/fact-check.md index 98a0714..ed9d574 100644 --- a/docs/specs/openhuman-setup-prd/fact-check.md +++ b/docs/specs/openhuman-setup-prd/fact-check.md @@ -45,11 +45,11 @@ label = "GonkaGate" endpoint = "https://api.gonkagate.com/v1" auth_style = "bearer" -chat_provider = "gonkagate:qwen/qwen3-235b-a22b-instruct-2507-fp8" -reasoning_provider = "gonkagate:moonshotai/kimi-k2.6" -agentic_provider = "gonkagate:moonshotai/kimi-k2.6" -coding_provider = "gonkagate:moonshotai/kimi-k2.6" -memory_provider = "gonkagate:qwen/qwen3-235b-a22b-instruct-2507-fp8" +chat_provider = "gonkagate:" +reasoning_provider = "gonkagate:" +agentic_provider = "gonkagate:" +coding_provider = "gonkagate:" +memory_provider = "gonkagate:" ``` The matching credential key is: @@ -104,6 +104,7 @@ provider:gonkagate / default ## Implementation Consequences - Use `cloud_providers` and workload provider fields as the main write path. +- Fetch concrete GonkaGate model IDs from authenticated `/v1/models`. - Do not write `api_url = "https://api.gonkagate.com/v1"` for v1. That is the old direct-provider mental model and risks confusing backend API resolution. - Do not rely on `model_routes` as the primary tier-routing mechanism. diff --git a/docs/specs/openhuman-setup-prd/implementation-plan.md b/docs/specs/openhuman-setup-prd/implementation-plan.md index 7ff89a5..aa1adc4 100644 --- a/docs/specs/openhuman-setup-prd/implementation-plan.md +++ b/docs/specs/openhuman-setup-prd/implementation-plan.md @@ -28,7 +28,7 @@ Done means: workload fields, and `provider:gonkagate` credentials. - Do not use the legacy `api_url` + `api_key` + `model_routes` path as the v1 write target. -- Do not add arbitrary base URLs or raw model ids. +- Do not add arbitrary base URLs or model ids absent from live `/v1/models`. - Do not install or patch OpenHuman. - Do not print, log, or place `gp-...` secrets in argv, output, repository files, or backup filenames. diff --git a/docs/specs/openhuman-setup-prd/release-proof.md b/docs/specs/openhuman-setup-prd/release-proof.md index f1b1033..7681791 100644 --- a/docs/specs/openhuman-setup-prd/release-proof.md +++ b/docs/specs/openhuman-setup-prd/release-proof.md @@ -5,8 +5,9 @@ Date: 2026-06-21 ## Automated Proof - `npm run ci` passes. -- Temp-workspace CLI smoke writes OpenHuman native GonkaGate config: - `OPENHUMAN_WORKSPACE=/tmp/openhuman-setup-smoke-codex node bin/gonkagate-openhuman.js --json --yes`. +- Temp-workspace CLI smoke writes OpenHuman native GonkaGate config with a + mocked authenticated `/v1/models` response: + `OPENHUMAN_WORKSPACE=/tmp/openhuman-setup-smoke-codex GONKAGATE_API_KEY=gp-... node bin/gonkagate-openhuman.js --json --yes`. - The smoke-written config contains one `cloud_providers` entry with slug `gonkagate` and workload provider strings for chat, reasoning, agentic, coding, and memory. @@ -17,7 +18,7 @@ Not completed in this environment. Current blocker: -- `GONKAGATE_API_KEY` is not set. +- A live `GONKAGATE_API_KEY` is not set. - OpenHuman discovery resolves to pre-login config: `/Users/daniil/.openhuman/users/local/config.toml`. - OpenHuman session inspection reports `missing`. @@ -35,8 +36,8 @@ and a real GonkaGate key entered through OpenHuman Settings -> AI. pre-login `users/local/config.toml`. - T2 Pure TOML Merge: fixture tests cover fresh config, unrelated provider preservation, GonkaGate replacement, and idempotency. -- T3 Model Selection: tests cover defaults, global `--model`, per-workload - overrides, and curated-key rejection. +- T3 Model Selection: tests cover live `/v1/models` parsing, first-live-model + defaults, global `--model`, per-workload overrides, and live-id rejection. - T4 Secret Intake And Redaction: tests cover `GONKAGATE_API_KEY`, `--api-key-stdin`, plain `--api-key` rejection, and `gp-...` redaction. - T5 Credential Storage: current safe outcome is no direct `auth-profiles.json` diff --git a/docs/specs/openhuman-setup-prd/spec.md b/docs/specs/openhuman-setup-prd/spec.md index e6e4110..9cf1f2b 100644 --- a/docs/specs/openhuman-setup-prd/spec.md +++ b/docs/specs/openhuman-setup-prd/spec.md @@ -64,15 +64,16 @@ tool-heavy work, coding, and summarization. - Preserve unrelated OpenHuman configuration. - Collect the GonkaGate API key through safe inputs only. - Let users select a GonkaGate model for each OpenHuman workload covered by v1. -- Provide recommended defaults that make `--yes` and non-interactive setup +- Provide live-catalog defaults that make `--yes` and non-interactive setup practical. - Verify that the resulting setup is effective before reporting success. -- Keep future model catalog changes centralized in the setup utility. +- Let future GonkaGate model catalog changes ship through `/v1/models`, without + repository changes. ## Non-Goals - Do not implement arbitrary custom base URLs in v1. -- Do not expose arbitrary non-curated model ids in v1. +- Do not use repository-hardcoded GonkaGate model catalogs in v1. - Do not mutate shell profiles or generate `.env` files. - Do not install or upgrade OpenHuman itself in v1. - Do not patch the OpenHuman binary at runtime. @@ -84,7 +85,7 @@ tool-heavy work, coding, and summarization. ### First-Time OpenHuman User Wants OpenHuman to work with GonkaGate quickly and safely. They should be able -to accept recommended defaults and return to OpenHuman. +to accept live-catalog defaults and return to OpenHuman. ### Agent Power User @@ -100,7 +101,7 @@ exposing secrets in command arguments. - As a user, I can run `npx @gonkagate/openhuman-setup` and configure OpenHuman without editing TOML manually. -- As a user, I can accept recommended model mappings for all OpenHuman tiers. +- As a user, I can accept live model mappings for all OpenHuman tiers. - As a user, I can pick a different validated GonkaGate model for `reasoning-v1`, `agentic-v1`, `coding-v1`, and `summarization-v1`. - As a user, I can run non-interactively with safe defaults. @@ -115,15 +116,16 @@ exposing secrets in command arguments. 1. Start CLI. 2. Detect OpenHuman installation/config context. 3. Resolve target OpenHuman `config.toml`. -4. Present or infer the role-model mapping. -5. Collect GonkaGate API key safely. -6. Create backups for managed config rewrites. -7. Merge GonkaGate settings into OpenHuman config. -8. Verify local config shape. -9. Store or verify GonkaGate credentials through OpenHuman's provider credential - shape. -10. Verify effective GonkaGate/OpenHuman inference path where possible. -11. Print next step: return to OpenHuman. +4. Collect GonkaGate API key safely. +5. Fetch the live GonkaGate `/v1/models` catalog. +6. Present or infer the role-model mapping. +7. Create backups for managed config rewrites. +8. Merge GonkaGate settings into OpenHuman config. +9. Verify local config shape. +10. Store or verify GonkaGate credentials through OpenHuman's provider + credential shape. +11. Verify effective GonkaGate/OpenHuman inference path where possible. +12. Print next step: return to OpenHuman. ## OpenHuman Config Discovery @@ -153,24 +155,22 @@ Compatibility gate: OpenHuman currently switches to a custom OpenAI-compatible LLM provider through slug-keyed `cloud_providers` entries and workload provider strings. v1 must write GonkaGate as a `cloud_providers` entry with slug `gonkagate` and must route selected workloads with provider strings such as -`gonkagate:moonshotai/kimi-k2.6`. +`gonkagate:`. The previous `api_url` + `api_key` direct-inference path is legacy fallback only and is out of scope for the primary v1 implementation. -## Curated Model Catalog +## Live Model Catalog -The v1 curated model catalog includes: +The runtime model catalog source of truth is authenticated +`GET https://api.gonkagate.com/v1/models` with the user's GonkaGate API key. The +setup utility parses the OpenAI-compatible response shape +`{ data: [{ id: string, name?: string }] }`, deduplicates model IDs, and rejects +empty or invalid responses. -| Key | Model id | Default use | -| ----------------------------------- | ---------------------------------------- | -------------------------------------- | -| `kimi-k2.6` | `moonshotai/kimi-k2.6` | Recommended general default | -| `qwen3-235b-a22b-instruct-2507-fp8` | `qwen/qwen3-235b-a22b-instruct-2507-fp8` | Recommended fast/summarization default | - -The catalog must stay centralized in code and covered by tests/docs. Future -models can be added when validated against OpenHuman's expected request shape. -Authenticated live catalog validation is required before release; -unauthenticated `GET /v1/models` currently returns `401`. +New or removed GonkaGate network models must not require repository changes. +`--yes` and non-interactive setup use the first returned live model unless the +API later exposes a supported default signal. ## Role Model Selection @@ -180,13 +180,13 @@ strings instead of the old `model_routes`-only path. ### Role Defaults -| OpenHuman concept | Config field | Recommended GonkaGate model | -| ------------------ | -------------------- | ---------------------------------------- | -| `reasoning-v1` | `reasoning_provider` | `moonshotai/kimi-k2.6` | -| `agentic-v1` | `agentic_provider` | `moonshotai/kimi-k2.6` | -| `coding-v1` | `coding_provider` | `moonshotai/kimi-k2.6` | -| `summarization-v1` | `memory_provider` | `qwen/qwen3-235b-a22b-instruct-2507-fp8` | -| fast/chat path | `chat_provider` | `qwen/qwen3-235b-a22b-instruct-2507-fp8` | +| OpenHuman concept | Config field | GonkaGate model source | +| ------------------ | -------------------- | ----------------------------- | +| `reasoning-v1` | `reasoning_provider` | selected live `/v1/models` id | +| `agentic-v1` | `agentic_provider` | selected live `/v1/models` id | +| `coding-v1` | `coding_provider` | selected live `/v1/models` id | +| `summarization-v1` | `memory_provider` | selected live `/v1/models` id | +| fast/chat path | `chat_provider` | selected live `/v1/models` id | Current OpenHuman source behavior to account for: @@ -201,8 +201,9 @@ Current OpenHuman source behavior to account for: ### Interactive Behavior -Interactive setup should show a model selection step for each covered workload. -The default highlighted option is the recommended model for that workload. +Interactive setup should select from the fetched `/v1/models` IDs for each +covered workload. The default highlighted option is the first live returned +model unless the API later exposes a supported default signal. The UI copy should explain the role briefly: @@ -216,20 +217,20 @@ The user can accept all defaults quickly or customize individual tiers. ### Non-Interactive Behavior -`--yes` uses all recommended defaults. +`--yes` uses the first model returned by the live `/v1/models` catalog. Future non-interactive flags should allow explicit tier overrides: ```bash npx @gonkagate/openhuman-setup \ - --reasoning-model kimi-k2.6 \ - --agentic-model kimi-k2.6 \ - --coding-model kimi-k2.6 \ - --summarization-model qwen3-235b-a22b-instruct-2507-fp8 + --reasoning-model provider/model-id \ + --agentic-model provider/model-id \ + --coding-model provider/model-id \ + --summarization-model provider/model-id ``` -All model override values must resolve to curated validated model keys. Raw -provider model ids are out of scope for v1. +All model override values must resolve to IDs from the fetched `/v1/models` +response. ## Config Write Contract @@ -241,11 +242,11 @@ The utility writes the minimum OpenHuman config needed for GonkaGate: - `endpoint = "https://api.gonkagate.com/v1"` - `auth_style = "bearer"` - workload provider strings such as: - - `chat_provider = "gonkagate:qwen/qwen3-235b-a22b-instruct-2507-fp8"` - - `reasoning_provider = "gonkagate:moonshotai/kimi-k2.6"` - - `agentic_provider = "gonkagate:moonshotai/kimi-k2.6"` - - `coding_provider = "gonkagate:moonshotai/kimi-k2.6"` - - `memory_provider = "gonkagate:qwen/qwen3-235b-a22b-instruct-2507-fp8"` + - `chat_provider = "gonkagate:"` + - `reasoning_provider = "gonkagate:"` + - `agentic_provider = "gonkagate:"` + - `coding_provider = "gonkagate:"` + - `memory_provider = "gonkagate:"` - GonkaGate credentials in OpenHuman's provider credential namespace: - provider key `provider:gonkagate` - profile `default` @@ -287,7 +288,7 @@ Verification should include: - GonkaGate `cloud_providers` entry exists exactly once. - API key is present in provider credentials without printing it. - Workload routing covers all v1 workloads. -- Selected models are curated validated GonkaGate models. +- Selected models come from the authenticated GonkaGate `/v1/models` response. - A real GonkaGate `/v1/chat/completions` smoke check passes when credentials and network allow it. - OpenHuman has an active app session, or the runtime session requirement is @@ -301,7 +302,7 @@ utility should additionally verify the effective OpenHuman client config. Errors must be actionable and non-leaky: - missing OpenHuman config: explain how config discovery was attempted. -- invalid model key: show valid curated keys. +- invalid model id: show valid live IDs from `/v1/models`. - unsafe secret flag: tell the user to use hidden prompt, `GONKAGATE_API_KEY`, or `--api-key-stdin`. - failed smoke check: show sanitized status/body and whether config was written. @@ -320,24 +321,24 @@ Planned flags: - `--yes` - `--json` - `--api-key-stdin` -- `--reasoning-model ` -- `--agentic-model ` -- `--coding-model ` -- `--summarization-model ` +- `--reasoning-model ` +- `--agentic-model ` +- `--coding-model ` +- `--summarization-model ` Explicitly unsupported: - `--api-key` - `--base-url` -- arbitrary raw model ids +- model ids absent from the live `/v1/models` response ## Success Metrics - A new user can complete setup without editing config files. - A power user can customize all four tier mappings in one run. - Reruns are idempotent and preserve unrelated OpenHuman settings. -- CI protects package contract, docs contract, curated model catalog, and role - defaults. +- CI protects package contract, docs contract, live model parsing, and role + routing. - Setup output never leaks the GonkaGate API key. ## Open Questions @@ -346,8 +347,7 @@ Explicitly unsupported: local RPC when the app is running and fall back to a manual key step? - Should `fast-v1` remain a separate CLI flag, or should v1 map it to the current OpenHuman `chat_provider` field? -- Should live model catalog validation require an API key, or should the setup - utility ship only the curated catalog until authenticated verification? +- Should the GonkaGate models API expose a preferred default signal for setup? - Should v1 be limited to an OpenHuman version range or commit range until the `cloud_providers` and provider credential shape stabilizes further? @@ -357,6 +357,6 @@ Explicitly unsupported: - PRD exists at `docs/specs/openhuman-setup-prd/spec.md`. - PRD documents the four required OpenHuman tiers. - PRD documents user-selectable tier mappings. -- PRD documents recommended defaults for each tier. +- PRD documents live-catalog defaults for each tier. - PRD documents safe secret inputs and the ban on `--api-key`. - PRD documents verification expectations beyond file writes. diff --git a/docs/specs/openhuman-setup-prd/tasks.md b/docs/specs/openhuman-setup-prd/tasks.md index 239641c..c3b577e 100644 --- a/docs/specs/openhuman-setup-prd/tasks.md +++ b/docs/specs/openhuman-setup-prd/tasks.md @@ -67,15 +67,15 @@ GonkaGate provider replacement, and rerun idempotency. ### T3 Model Selection -- [ ] Keep model choices limited to the curated catalog. -- [ ] Keep `--model` as a global curated-model shortcut for the current scaffold +- [ ] Keep model choices limited to authenticated `/v1/models`. +- [ ] Keep `--model` as a global live-model shortcut for the current scaffold contract. - [ ] Implement explicit flags: `--reasoning-model`, `--agentic-model`, `--coding-model`, `--summarization-model`. -- [ ] Reject unknown model keys with valid choices. +- [ ] Reject unknown model ids with valid live choices. -Proof: tests cover default model selection, per-workload overrides, and invalid -model rejection. +Proof: tests cover live catalog parsing, default model selection, per-workload +overrides, and invalid model rejection. ### T4 Secret Intake And Redaction @@ -152,7 +152,7 @@ Stop and report evidence instead of guessing if: - current OpenHuman no longer uses the documented `cloud_providers` shape; - credential-store writes cannot be proven safe; - `smol-toml` cannot preserve the required semantics; -- GonkaGate rejects both curated default models during authenticated smoke; +- GonkaGate rejects the selected live model during authenticated smoke; - OpenHuman app-session detection cannot be made reliable enough to report. ## Progress Log Format diff --git a/src/cli/execute.ts b/src/cli/execute.ts index 6eafc6f..061cc04 100644 --- a/src/cli/execute.ts +++ b/src/cli/execute.ts @@ -1,3 +1,4 @@ +import { select } from "@inquirer/prompts"; import { CONTRACT_METADATA } from "../constants/contract.js"; import { GONKAGATE_AUTH_STYLE, @@ -9,8 +10,10 @@ import { GONKAGATE_PROVIDER_NAME, } from "../constants/gateway.js"; import { + fetchGonkaGateModels, + getDefaultGonkaGateModel, resolveOpenHumanWorkloadProviders, - getRecommendedValidatedModel, + type GonkaGateModel, } from "../constants/models.js"; import { credentialOutcome, @@ -30,28 +33,42 @@ import type { CliOptions, CliResult } from "./contracts.js"; export interface CliRuntime extends SecretRuntime { fetch?: typeof fetch; now?: () => Date; + promptModel?: (models: readonly GonkaGateModel[]) => Promise; } export async function executeCli( options: CliOptions, runtime: CliRuntime = {}, ): Promise { - const model = getRecommendedValidatedModel(options.modelKey); + const apiKey = await collectApiKey(options, runtime); + if (apiKey === undefined) { + throw new Error( + "GonkaGate API key is required to fetch available models; use hidden prompt, GONKAGATE_API_KEY, or --api-key-stdin", + ); + } + + const liveModels = await fetchGonkaGateModels(apiKey.value, runtime.fetch); + const globalModelKey = await collectGlobalModelKey( + options, + runtime, + liveModels, + ); + const model = getDefaultGonkaGateModel(liveModels, globalModelKey); const workloadProviders = resolveOpenHumanWorkloadProviders( omitUndefined({ agenticModelKey: options.agenticModelKey, codingModelKey: options.codingModelKey, - globalModelKey: options.modelKey, + globalModelKey, reasoningModelKey: options.reasoningModelKey, summarizationModelKey: options.summarizationModelKey, }), + liveModels, ); const discovery = await discoverOpenHumanConfig( runtime.env === undefined ? {} : { env: runtime.env }, ); const existingConfig = await readConfigIfExists(discovery.configPath); const mergedConfig = mergeGonkaGateConfig(existingConfig, workloadProviders); - const apiKey = await collectApiKey(options, runtime); const config = await writeConfigAtomically( discovery.configPath, mergedConfig, @@ -67,9 +84,9 @@ export async function executeCli( ); const reasoningModel = workloadProviders.find((provider) => provider.workload === "reasoning") - ?.model ?? model.modelId; + ?.model ?? model.id; const gonkaGateSmoke = await runGonkaGateSmoke( - apiKey?.value, + apiKey.value, reasoningModel, runtime.fetch, ); @@ -112,7 +129,7 @@ export async function executeCli( profile: GONKAGATE_CREDENTIAL_PROFILE, providerKey: GONKAGATE_CREDENTIAL_PROVIDER_KEY, }, - defaultModel: model.modelId, + defaultModel: model.id, workloadProviders: workloadProviders.map((provider) => ({ ...provider, })), @@ -130,6 +147,45 @@ export async function executeCli( }; } +async function collectGlobalModelKey( + options: CliOptions, + runtime: CliRuntime, + models: readonly GonkaGateModel[], +): Promise { + if (options.modelKey !== undefined || hasWorkloadOverride(options)) { + return options.modelKey; + } + + const canPrompt = + !options.yes && + !options.json && + (runtime.isInteractive ?? (process.stdin.isTTY && process.stdout.isTTY)); + if (!canPrompt) { + return undefined; + } + + if (runtime.promptModel !== undefined) { + return runtime.promptModel(models); + } + + return select({ + choices: models.map((model) => ({ + name: model.name === undefined ? model.id : `${model.name} (${model.id})`, + value: model.id, + })), + message: "GonkaGate model", + }); +} + +function hasWorkloadOverride(options: CliOptions): boolean { + return ( + options.agenticModelKey !== undefined || + options.codingModelKey !== undefined || + options.reasoningModelKey !== undefined || + options.summarizationModelKey !== undefined + ); +} + function buildWarnings( credentials: ReturnType, sessionStatus: "missing" | "present" | "unknown", diff --git a/src/cli/parse.ts b/src/cli/parse.ts index 7ad0386..938ac7c 100644 --- a/src/cli/parse.ts +++ b/src/cli/parse.ts @@ -1,6 +1,5 @@ import { Command, Option } from "commander"; import { CONTRACT_METADATA } from "../constants/contract.js"; -import { getValidatedModelKeys } from "../constants/models.js"; import type { CliOptions } from "./contracts.js"; export function parseCliArgs(argv: readonly string[]): CliOptions { @@ -20,34 +19,30 @@ export function parseCliArgs(argv: readonly string[]): CliOptions { .option("--api-key-stdin", "read the GonkaGate API key from stdin") .option("--json", "render machine-readable JSON output") .option("--yes", "accept safe defaults for non-interactive setup") - .addOption( - new Option("--model ", "curated GonkaGate model key").choices( - getValidatedModelKeys(), - ), - ) + .addOption(new Option("--model ", "GonkaGate model id from /v1/models")) .addOption( new Option( - "--reasoning-model ", - "curated model key for OpenHuman reasoning workloads", - ).choices(getValidatedModelKeys()), + "--reasoning-model ", + "GonkaGate model id for OpenHuman reasoning workloads", + ), ) .addOption( new Option( - "--agentic-model ", - "curated model key for OpenHuman agentic workloads", - ).choices(getValidatedModelKeys()), + "--agentic-model ", + "GonkaGate model id for OpenHuman agentic workloads", + ), ) .addOption( new Option( - "--coding-model ", - "curated model key for OpenHuman coding workloads", - ).choices(getValidatedModelKeys()), + "--coding-model ", + "GonkaGate model id for OpenHuman coding workloads", + ), ) .addOption( new Option( - "--summarization-model ", - "curated model key for OpenHuman summarization workloads", - ).choices(getValidatedModelKeys()), + "--summarization-model ", + "GonkaGate model id for OpenHuman summarization workloads", + ), ); command.parse(argv, { from: "node" }); diff --git a/src/constants/gateway.ts b/src/constants/gateway.ts index b959c5d..e51366d 100644 --- a/src/constants/gateway.ts +++ b/src/constants/gateway.ts @@ -1,5 +1,6 @@ export const GONKAGATE_BASE_URL = "https://api.gonkagate.com/v1"; export const GONKAGATE_CHAT_COMPLETIONS_URL = `${GONKAGATE_BASE_URL}/chat/completions`; +export const GONKAGATE_MODELS_URL = `${GONKAGATE_BASE_URL}/models`; export const GONKAGATE_AUTH_STYLE = "bearer"; export const GONKAGATE_CREDENTIAL_PROFILE = "default"; export const GONKAGATE_CREDENTIAL_PROVIDER_KEY = "provider:gonkagate"; diff --git a/src/constants/models.ts b/src/constants/models.ts index 88024da..cb84334 100644 --- a/src/constants/models.ts +++ b/src/constants/models.ts @@ -1,33 +1,9 @@ -import { GONKAGATE_PROVIDER_ID } from "./gateway.js"; +import { GONKAGATE_MODELS_URL, GONKAGATE_PROVIDER_ID } from "./gateway.js"; +import { redactSecrets } from "../util/redact.js"; -export interface CuratedModelDefinition { - displayName: string; - modelId: string; - recommended: boolean; - validationStatus: "validated"; -} - -export type CuratedModelRegistry = Record; - -export const CURATED_MODEL_REGISTRY = Object.freeze({ - "qwen3-235b-a22b-instruct-2507-fp8": { - displayName: "Qwen3 235B A22B Instruct 2507 FP8", - modelId: "qwen/qwen3-235b-a22b-instruct-2507-fp8", - recommended: false, - validationStatus: "validated", - }, - "kimi-k2.6": { - displayName: "Kimi K2.6", - modelId: "moonshotai/kimi-k2.6", - recommended: true, - validationStatus: "validated", - }, -} as const satisfies CuratedModelRegistry); - -export type CuratedModelKey = keyof typeof CURATED_MODEL_REGISTRY; - -export interface CuratedModelRecord extends CuratedModelDefinition { - key: CuratedModelKey; +export interface GonkaGateModel { + id: string; + name?: string; } export type WorkloadId = "chat" | "reasoning" | "agentic" | "coding" | "memory"; @@ -52,50 +28,6 @@ export interface ModelSelectionInput { summarizationModelKey?: string; } -const recommendedGeneralModel = "moonshotai/kimi-k2.6"; -const recommendedFastModel = "qwen/qwen3-235b-a22b-instruct-2507-fp8"; - -export const DEFAULT_OPENHUMAN_WORKLOAD_PROVIDERS = Object.freeze([ - { - configField: "chat_provider", - model: recommendedFastModel, - providerString: `${GONKAGATE_PROVIDER_ID}:${recommendedFastModel}`, - workload: "chat", - }, - { - configField: "reasoning_provider", - model: recommendedGeneralModel, - providerString: `${GONKAGATE_PROVIDER_ID}:${recommendedGeneralModel}`, - workload: "reasoning", - }, - { - configField: "agentic_provider", - model: recommendedGeneralModel, - providerString: `${GONKAGATE_PROVIDER_ID}:${recommendedGeneralModel}`, - workload: "agentic", - }, - { - configField: "coding_provider", - model: recommendedGeneralModel, - providerString: `${GONKAGATE_PROVIDER_ID}:${recommendedGeneralModel}`, - workload: "coding", - }, - { - configField: "memory_provider", - model: recommendedFastModel, - providerString: `${GONKAGATE_PROVIDER_ID}:${recommendedFastModel}`, - workload: "memory", - }, -] as const satisfies readonly WorkloadProvider[]); - -const defaultModelKeys = { - agentic: "kimi-k2.6", - chat: "qwen3-235b-a22b-instruct-2507-fp8", - coding: "kimi-k2.6", - memory: "qwen3-235b-a22b-instruct-2507-fp8", - reasoning: "kimi-k2.6", -} as const satisfies Record; - const workloadFields = { agentic: "agentic_provider", chat: "chat_provider", @@ -112,66 +44,121 @@ const workloadOrder = [ "memory", ] as const satisfies readonly WorkloadId[]; -export function getValidatedModelKeys(): CuratedModelKey[] { - return Object.keys(CURATED_MODEL_REGISTRY) as CuratedModelKey[]; -} +export async function fetchGonkaGateModels( + apiKey: string, + fetchImpl: typeof fetch = fetch, +): Promise { + const response = await fetchImpl(GONKAGATE_MODELS_URL, { + headers: { + Accept: "application/json", + Authorization: `Bearer ${apiKey}`, + }, + method: "GET", + signal: AbortSignal.timeout(20_000), + }); -export function getCuratedModelByKey( - key: string | undefined, -): CuratedModelRecord | undefined { - if (key === undefined || !(key in CURATED_MODEL_REGISTRY)) { - return undefined; + if (!response.ok) { + const body = redactSecrets((await response.text()).slice(0, 500)); + const detail = body.length === 0 ? response.statusText : body; + throw new Error( + `Failed to fetch GonkaGate models (${response.status}): ${detail}`, + ); } - const modelKey = key as CuratedModelKey; - return { - ...CURATED_MODEL_REGISTRY[modelKey], - key: modelKey, - }; + let payload: unknown; + try { + payload = await response.json(); + } catch (error) { + const message = error instanceof Error ? error.message : String(error); + throw new Error( + `GonkaGate models response was not valid JSON: ${redactSecrets(message)}`, + ); + } + + return parseGonkaGateModels(payload); } -export function getRecommendedValidatedModel( - requestedKey?: string, -): CuratedModelRecord { - const requested = getCuratedModelByKey(requestedKey); - if (requested !== undefined) { - return requested; +export function parseGonkaGateModels(payload: unknown): GonkaGateModel[] { + if (!isRecord(payload) || !Array.isArray(payload.data)) { + throw new Error("GonkaGate models response was invalid."); } - for (const key of getValidatedModelKeys()) { - const model = CURATED_MODEL_REGISTRY[key]; - if (model.recommended) { - return { - ...model, - key, - }; + const seen = new Set(); + const models: GonkaGateModel[] = []; + + for (const item of payload.data) { + if (!isRecord(item) || typeof item.id !== "string") { + throw new Error("GonkaGate models response contained an invalid model."); + } + + const id = item.id.trim(); + if (id.length === 0) { + throw new Error("GonkaGate models response contained an empty model id."); + } + + if (seen.has(id)) { + continue; + } + + seen.add(id); + const name = + typeof item.name === "string" && item.name.trim().length > 0 + ? item.name.trim() + : undefined; + const model: GonkaGateModel = { id }; + if (name !== undefined) { + model.name = name; } + models.push(model); + } + + if (models.length === 0) { + throw new Error("GonkaGate models response did not include any models."); + } + + return models; +} + +export function getDefaultGonkaGateModel( + models: readonly GonkaGateModel[], + requestedId?: string, +): GonkaGateModel { + if (requestedId !== undefined) { + return getRequiredGonkaGateModel(models, requestedId); } - throw new Error("No recommended validated GonkaGate model is configured."); + const model = models[0]; + if (model === undefined) { + throw new Error("GonkaGate models response did not include any models."); + } + return model; } export function resolveOpenHumanWorkloadProviders( input: ModelSelectionInput = {}, + models: readonly GonkaGateModel[], ): WorkloadProvider[] { - const keyFor = (workload: WorkloadId): string | undefined => { + const defaultModel = getDefaultGonkaGateModel( + models, + input.globalModelKey, + ).id; + const idFor = (workload: WorkloadId): string => { switch (workload) { case "agentic": - return input.agenticModelKey ?? input.globalModelKey; + return input.agenticModelKey ?? defaultModel; case "coding": - return input.codingModelKey ?? input.globalModelKey; + return input.codingModelKey ?? defaultModel; case "memory": - return input.summarizationModelKey ?? input.globalModelKey; + return input.summarizationModelKey ?? defaultModel; case "reasoning": - return input.reasoningModelKey ?? input.globalModelKey; + return input.reasoningModelKey ?? defaultModel; case "chat": - return input.globalModelKey; + return defaultModel; } }; return workloadOrder.map((workload) => { - const key = keyFor(workload) ?? defaultModelKeys[workload]; - const model = getRequiredCuratedModel(key).modelId; + const model = getRequiredGonkaGateModel(models, idFor(workload)).id; return { configField: workloadFields[workload], model, @@ -181,13 +168,22 @@ export function resolveOpenHumanWorkloadProviders( }); } -function getRequiredCuratedModel(key: string): CuratedModelRecord { - const model = getCuratedModelByKey(key); +function getRequiredGonkaGateModel( + models: readonly GonkaGateModel[], + id: string, +): GonkaGateModel { + const model = models.find((candidate) => candidate.id === id); if (model !== undefined) { return model; } throw new Error( - `Unknown GonkaGate model key. Valid choices: ${getValidatedModelKeys().join(", ")}`, + `Unknown GonkaGate model id. Valid choices: ${models + .map((candidate) => candidate.id) + .join(", ")}`, ); } + +function isRecord(value: unknown): value is Record { + return typeof value === "object" && value !== null; +} diff --git a/test/cli.test.mjs b/test/cli.test.mjs index 6b8d8a1..053514b 100644 --- a/test/cli.test.mjs +++ b/test/cli.test.mjs @@ -1,8 +1,9 @@ import assert from "node:assert/strict"; import { execFile } from "node:child_process"; -import { mkdtemp, readFile, rm } from "node:fs/promises"; +import { mkdtemp, readFile, rm, writeFile } from "node:fs/promises"; import { tmpdir } from "node:os"; import { join } from "node:path"; +import { pathToFileURL } from "node:url"; import { promisify } from "node:util"; import { parse } from "smol-toml"; import { executeCli } from "../dist/cli/execute.js"; @@ -14,13 +15,33 @@ const tempRoot = await mkdtemp(join(tmpdir(), "openhuman-setup-cli-")); try { const workspace = join(tempRoot, "env-root"); + const mockFetchPath = join(tempRoot, "mock-fetch.mjs"); + await writeFile( + mockFetchPath, + ` +globalThis.fetch = async (url, init) => { + const href = String(url); + if (init?.headers?.Authorization !== "Bearer gp-test-key") { + return new Response("bad auth", { status: 401 }); + } + if (href === "https://api.gonkagate.com/v1/models") { + return Response.json({ data: [{ id: "live/default-model", name: "Default" }, { id: "live/extra-model" }] }); + } + if (href === "https://api.gonkagate.com/v1/chat/completions") { + return Response.json({ ok: true }); + } + return new Response("not found", { status: 404 }); +}; +`, + ); const { stdout } = await execFileAsync( process.execPath, ["bin/gonkagate-openhuman.js", "--json", "--yes"], { env: { ...process.env, - GONKAGATE_API_KEY: "", + GONKAGATE_API_KEY: "gp-test-key", + NODE_OPTIONS: `--import=${pathToFileURL(mockFetchPath).href}`, OPENHUMAN_WORKSPACE: workspace, }, }, @@ -41,16 +62,54 @@ try { assert.equal(result.credentials.written, false); assert.equal(result.credentials.status, "manual_required"); assert.equal(result.verification.localConfig.status, "passed"); - assert.equal(result.verification.gonkaGateSmoke.status, "skipped"); + assert.equal(result.verification.gonkaGateSmoke.status, "passed"); const config = parse(await readFile(join(workspace, "config.toml"), "utf8")); assert.equal(config.api_url, undefined); assert.equal(config.cloud_providers.length, 1); assert.equal(config.cloud_providers[0].slug, "gonkagate"); - assert.equal( - config.chat_provider, - "gonkagate:qwen/qwen3-235b-a22b-instruct-2507-fp8", + assert.equal(config.chat_provider, "gonkagate:live/default-model"); + + const overrideWorkspace = join(tempRoot, "override-workspace"); + const overrideOptions = parseCliArgs([ + "node", + "gonkagate-openhuman", + "--json", + "--yes", + "--api-key-stdin", + "--model", + "live/extra-model", + ]); + const overrideResult = await executeCli(overrideOptions, { + env: { OPENHUMAN_WORKSPACE: overrideWorkspace }, + fetch: liveFetch, + readStdin: async () => "gp-test-key\n", + }); + assert.equal(overrideResult.ok, true); + const overrideConfig = parse( + await readFile(join(overrideWorkspace, "config.toml"), "utf8"), ); + assert.equal(overrideConfig.chat_provider, "gonkagate:live/extra-model"); + assert.equal(overrideConfig.reasoning_provider, "gonkagate:live/extra-model"); + + const interactiveWorkspace = join(tempRoot, "interactive-workspace"); + const interactiveResult = await executeCli( + parseCliArgs(["node", "gonkagate-openhuman"]), + { + env: { + GONKAGATE_API_KEY: "gp-test-key", + OPENHUMAN_WORKSPACE: interactiveWorkspace, + }, + fetch: liveFetch, + isInteractive: true, + promptModel: async (models) => models[1].id, + }, + ); + assert.equal(interactiveResult.ok, true); + const interactiveConfig = parse( + await readFile(join(interactiveWorkspace, "config.toml"), "utf8"), + ); + assert.equal(interactiveConfig.chat_provider, "gonkagate:live/extra-model"); const secretWorkspace = join(tempRoot, "secret-workspace"); const options = parseCliArgs([ @@ -62,8 +121,12 @@ try { ]); const secretResult = await executeCli(options, { env: { OPENHUMAN_WORKSPACE: secretWorkspace }, - fetch: async () => - new Response("upstream rejected gp-test-secret-123", { status: 401 }), + fetch: async (url) => + String(url) === "https://api.gonkagate.com/v1/models" + ? Response.json({ data: [{ id: "live/redaction-model" }] }) + : new Response("upstream rejected gp-test-secret-123", { + status: 401, + }), readStdin: async () => "gp-test-secret-123\n", }); const rendered = JSON.stringify(secretResult); @@ -74,3 +137,20 @@ try { } finally { await rm(tempRoot, { force: true, recursive: true }); } + +async function liveFetch(url, init) { + assert.equal(init?.headers?.Authorization, "Bearer gp-test-key"); + const href = String(url); + if (href === "https://api.gonkagate.com/v1/models") { + return Response.json({ + data: [ + { id: "live/default-model", name: "Default" }, + { id: "live/extra-model", name: "Extra" }, + ], + }); + } + if (href === "https://api.gonkagate.com/v1/chat/completions") { + return Response.json({ ok: true }); + } + return new Response("not found", { status: 404 }); +} diff --git a/test/docs-contract.test.mjs b/test/docs-contract.test.mjs index d20574d..1a4427f 100644 --- a/test/docs-contract.test.mjs +++ b/test/docs-contract.test.mjs @@ -12,8 +12,8 @@ const tasks = await readFile("docs/specs/openhuman-setup-prd/tasks.md", "utf8"); assert.match(readme, /@gonkagate\/openhuman-setup/); assert.match(readme, /https:\/\/api\.gonkagate\.com\/v1/); -assert.match(readme, /moonshotai\/kimi-k2\.6/); -assert.match(readme, /qwen\/qwen3-235b-a22b-instruct-2507-fp8/); +assert.match(readme, /\/v1\/models/); +assert.doesNotMatch(readme, /curated GonkaGate model IDs/); assert.match(security, /GONKAGATE_API_KEY/); assert.match(security, /--api-key-stdin/); assert.match(security, /must not accept a plain `--api-key` flag/); @@ -22,6 +22,7 @@ assert.match(readme, /docs\/specs\/openhuman-setup-prd\/spec\.md/); assert.match(prd, /cloud_providers/); assert.match(prd, /provider:gonkagate/); assert.match(prd, /chat_provider/); +assert.match(prd, /\/v1\/models/); for (const tier of [ "reasoning-v1", "agentic-v1", @@ -30,10 +31,9 @@ for (const tier of [ ]) { assert.match(prd, new RegExp(tier)); } -assert.match(prd, /moonshotai\/kimi-k2\.6/); -assert.match(prd, /qwen\/qwen3-235b-a22b-instruct-2507-fp8/); -assert.match(prd, /--reasoning-model /); -assert.match(prd, /--summarization-model /); +assert.match(prd, /--reasoning-model /); +assert.match(prd, /--summarization-model /); +assert.doesNotMatch(prd, /Curated Model Catalog/); assert.match(prd, /Setup success must not mean only "file write completed."/); assert.match(prd, /credential-store boundary/); assert.match(releaseProof, /Automated Proof/); diff --git a/test/model-selection.test.mjs b/test/model-selection.test.mjs index eb0c8d0..9e62d8c 100644 --- a/test/model-selection.test.mjs +++ b/test/model-selection.test.mjs @@ -1,41 +1,64 @@ import assert from "node:assert/strict"; -import { resolveOpenHumanWorkloadProviders } from "../dist/constants/models.js"; +import { + parseGonkaGateModels, + resolveOpenHumanWorkloadProviders, +} from "../dist/constants/models.js"; import { parseCliArgs } from "../dist/cli/parse.js"; -const defaults = resolveOpenHumanWorkloadProviders(); +const liveModels = parseGonkaGateModels({ + data: [ + { id: "live/default-model", name: "Default Model" }, + { id: "live/default-model", name: "Duplicate ignored" }, + { id: "live/extra-model", name: "Extra Model" }, + ], +}); + +assert.deepEqual(liveModels, [ + { id: "live/default-model", name: "Default Model" }, + { id: "live/extra-model", name: "Extra Model" }, +]); + +const defaults = resolveOpenHumanWorkloadProviders({}, liveModels); assert.equal( defaults.find((provider) => provider.workload === "reasoning").providerString, - "gonkagate:moonshotai/kimi-k2.6", -); -assert.equal( - defaults.find((provider) => provider.workload === "memory").providerString, - "gonkagate:qwen/qwen3-235b-a22b-instruct-2507-fp8", + "gonkagate:live/default-model", ); -const global = resolveOpenHumanWorkloadProviders({ - globalModelKey: "qwen3-235b-a22b-instruct-2507-fp8", -}); -assert.ok( - global.every( - (provider) => provider.model === "qwen/qwen3-235b-a22b-instruct-2507-fp8", - ), +const global = resolveOpenHumanWorkloadProviders( + { + globalModelKey: "live/extra-model", + }, + liveModels, ); +assert.ok(global.every((provider) => provider.model === "live/extra-model")); -const override = resolveOpenHumanWorkloadProviders({ - codingModelKey: "qwen3-235b-a22b-instruct-2507-fp8", -}); +const override = resolveOpenHumanWorkloadProviders( + { + codingModelKey: "live/extra-model", + }, + liveModels, +); assert.equal( override.find((provider) => provider.workload === "coding").providerString, - "gonkagate:qwen/qwen3-235b-a22b-instruct-2507-fp8", + "gonkagate:live/extra-model", ); assert.equal( override.find((provider) => provider.workload === "reasoning").providerString, - "gonkagate:moonshotai/kimi-k2.6", + "gonkagate:live/default-model", ); assert.throws( - () => resolveOpenHumanWorkloadProviders({ globalModelKey: "raw/model" }), - /Valid choices: .*kimi-k2\.6/, + () => + resolveOpenHumanWorkloadProviders( + { globalModelKey: "missing/model" }, + liveModels, + ), + /Valid choices: live\/default-model, live\/extra-model/, +); + +assert.equal( + parseCliArgs(["node", "cmd", "--model", "unknown/live-model"]).modelKey, + "unknown/live-model", ); assert.throws( diff --git a/test/toml-merge.test.mjs b/test/toml-merge.test.mjs index 5291c30..211f707 100644 --- a/test/toml-merge.test.mjs +++ b/test/toml-merge.test.mjs @@ -6,7 +6,9 @@ import { verifyGonkaGateConfig, } from "../dist/openhuman/toml.js"; -const providers = resolveOpenHumanWorkloadProviders(); +const providers = resolveOpenHumanWorkloadProviders({}, [ + { id: "live/default-model" }, +]); const fresh = mergeGonkaGateConfig("", providers); const freshConfig = parse(fresh); @@ -16,7 +18,7 @@ assert.equal( freshConfig.cloud_providers[0].endpoint, "https://api.gonkagate.com/v1", ); -assert.equal(freshConfig.reasoning_provider, "gonkagate:moonshotai/kimi-k2.6"); +assert.equal(freshConfig.reasoning_provider, "gonkagate:live/default-model"); assert.deepEqual(verifyGonkaGateConfig(fresh, providers), []); const existing = `