Skip to content

Sync models without an API key (public endpoint) + use real API metadata - #5

Draft
KRoperUK wants to merge 4 commits into
brent-weatherall:mainfrom
KRoperUK:feat/dynamic-models-public-endpoint
Draft

Sync models without an API key (public endpoint) + use real API metadata#5
KRoperUK wants to merge 4 commits into
brent-weatherall:mainfrom
KRoperUK:feat/dynamic-models-public-endpoint

Conversation

@KRoperUK

Copy link
Copy Markdown

Stacked on #1. This branch contains the commits from #1 (feat/auto-sync-models) plus one commit on top. Review only the final commit (Sync models without an API key…); once #1 merges, this diff reduces to that commit. Targeting main so it can land after #1.

Builds directly on @augustoolucas's auto-sync work in #1 and tightens three things in how the runtime sync discovers models.

1. Sync works without an API key (the big one)

#1 gates the fetch behind COMMANDCODE_API_KEY:

const apiKey = process.env.COMMANDCODE_API_KEY
if (!apiKey) return null

But /provider/v1/models is a public, unauthenticated endpoint (verified: it returns the full catalog with no Authorization header). Most users authenticate via /connect, which does not export COMMANDCODE_API_KEY — so as written, auto-sync silently does nothing for them. This change runs the request unauthenticated when no key is present, and still sends Bearer <key> when one is:

headers: apiKey ? { Authorization: `Bearer ${apiKey}` } : {},

2. Use the display name the API returns

The endpoint returns a name field (e.g. "MiMo V2.5 Pro"), but #1 derives the name from the id (id.split("/").pop()"mimo-v2.5-pro"). ApiModel now captures name and mergeModels prefers it, falling back to the id segment.

3. Correct tier + honest pricing for API-only models

For models present in the API but not in the bundled models.json, #1 hardcodes tier: "open-source" and cost: { input: 0.5, output: 2 }. Tier is now derived from the id namespace (bare ids like gpt-*/claude-* are premium; namespaced are open-source), and the invented price is replaced with 0/0 so cost accounting isn't fed a fabricated rate.

Note: tier isn't currently emitted into the opencode model config (the config hook copies id/name/reasoning/tool_call/cost/limit), and API-only models aren't persisted, so the tier change is internal-correctness only today. I kept it because hardcoding every new model to open-source is simply wrong and the derivation is free — and tier does map to real Command Code plan access (open-source = usable on the Go plan; premium = Pro and above).

Tests

  • Reworked the former "falls back to local when API key is missing" test → now asserts the fetch still happens with no Authorization header and merges API-only models.
  • Added a test that the API-provided name is used for new models.
  • bun test tests/unit/87 pass, 0 fail; tsc --noEmit clean.

🤖 Generated with Claude Code

augustoolucas and others added 4 commits June 4, 2026 12:28
- Fetch model list from CommandCode API on plugin config hook
- Merge with local models.json (API provides context_length, local provides curated cost/tier/reasoning)
- Persist merged result back to models.json for offline fallback
- Treat empty API response as failure to avoid wiping local models
- Add idempotency to saveModels (only write when content changed)
- Add new models and fix context sizes in models.json
- Add unit tests for mergeModels (pure) and integration tests using existing mockFetch helpers
- Switch from disk write to in-memory merge; sync updates no longer
  persist between startups
- Add config-file opt-out at
  ~/.config/opencode/commandcode-go-opencode-provider.json
  with `disableModelSync: true` to skip the API fetch entirely
- loadModels: throw actionable error on missing/corrupt bundled
  models.json ("please reinstall commandcode-go-opencode-provider",
  original error chained via Error.cause)
- Tests: add withFakeConfig / withMissingModels / withCorruptModels
  helpers and structural assertions for bundled, opt-out, and
  load-error paths
- README: document the opt-out workflow and bundled-list refresh
  via `bun run sync`
…adata

Builds on the auto-sync work. Three improvements to how the runtime sync
discovers models:

- Drop the COMMANDCODE_API_KEY requirement for the model fetch. The listing
  endpoint (/provider/v1/models) is public, so requiring the env var meant
  users who authenticate via /connect (the common case — it doesn't export the
  env var) got no auto-sync at all. The request now runs unauthenticated when no
  key is present, and still sends the key as a Bearer token when it is.
- Use the display name the API returns for new API-only models, instead of
  deriving it from the id (e.g. "MiMo V2.5 Pro" rather than "mimo-v2.5-pro").
- Derive tier from the id namespace instead of hardcoding "open-source", and
  stop fabricating a 0.5/2 price for unpriced models (zeroed instead, so cost
  accounting isn't fed an invented rate).

Tests: rework the "missing API key" case to assert the fetch still happens with
no Authorization header and merges API-only models; add a test for the
API-provided name. 87 pass, typecheck clean.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants