Skip to content

fix: resolve the qwen model catalog from the gateway, not qwen-code - #313

Merged
saucam merged 2 commits into
mainfrom
fix/qwen-model-catalog
Sep 1, 2026
Merged

fix: resolve the qwen model catalog from the gateway, not qwen-code#313
saucam merged 2 commits into
mainfrom
fix/qwen-model-catalog

Conversation

@saucam

@saucam saucam commented Sep 1, 2026

Copy link
Copy Markdown
Collaborator

Problem

The model picker rendered empty for the qwen backend, against a gateway serving a dozen models.

The cause is not codeoid's caching. qwen-code's getAvailableModels() is not a catalog fetch — it returns modelRegistry.getModelsForAuthType(currentAuthType), an in-memory registry seeded once at construction from:

  • hardcoded QWEN_OAUTH_MODELS (a single entry, coder-model) for qwen-oauth
  • the user's modelProviders setting for every other authType

Under authType: "openai" with no modelProviders declared it returns []. Verified against the live SDK:

$ probe against @qwen-code/sdk 0.1.8, authType=openai
RAW: { "subtype": "get_available_models", "models": [] }

qwen-code never contacts the gateway, so there was nothing for codeoid to cache — _cacheModels correctly ignores empty reports. Declaring modelProviders in ~/.qwen/settings.json fixes it, but that is a hand-maintained list that goes stale and has to be repeated on every box.

Change

QwenProvider asks the gateway itself. On the openai path it is an OpenAI-compatible endpoint, so GET /models is authoritative and stays current as the provider adds models.

The qwen-code registry is unioned in rather than replaced: it carries real display labels, and qwen-oauth has a dynamic base URL and no key to present, so HTTP is not an option there. A failed fetch falls back to the registry and logs once — the session still runs, only the picker is poorer.

Everything the endpoint reports is surfaced. The response carries only {id, object, created, owned_by} — no modality field — so filtering out image/audio entries would mean pattern-matching ids, which is the same hardcoded-list problem this removes.

Two supporting fixes:

  • normalizeModelCatalog read the wrong field. It looked for name, but the SDK emits label (its handler projects the registry entry down to {id, label, capabilities, contextWindowSize}, also dropping description). Every model rendered as its raw id. name / modelId / availableModels are kept as accepted aliases so a future rename doesn't silently empty the picker again.
  • _cacheModels was first-non-empty-wins per daemon lifetime. Providers report on each query-loop build, so a model added to a gateway now appears on the next session instead of after a daemon restart. Empty reports are still ignored so a failed fetch cannot clobber a good catalog, and an unchanged report skips the SQLite write.

Verification

Against the real Bailian token-plan gateway, with modelProviders removed from ~/.qwen/settings.json:

live /models  : 12 → qwen3.7-max, qwen3.7-plus, qwen3.6-flash, glm-5.2,
                deepseek-v4-pro, wan2.7-image, wan2.7-image-pro,
                qwen-audio-3.0-tts-plus, deepseek-v4-flash-0731,
                qwen3.8-max, qwen-audio-3.0-realtime-plus, qwen3.8-flash
unioned       : 12
OK
  • bun test — 2411 pass, 19 skip, 0 fail (161 files)
  • bun run typecheck — clean
  • bun run lint — clean

New coverage: the real SDK 0.1.8 response shape, fetchOpenAiModelCatalog (list shape, trailing-slash base URLs, non-2xx, junk payloads), unionCatalogs (dedupe, label preference, registry-only models, ordering), and both _cacheModels cache semantics.

🤖 Generated with Claude Code

The model picker rendered empty for the qwen backend against a gateway
serving a dozen models.

qwen-code's `getAvailableModels()` is not a catalog fetch. It returns
`modelRegistry.getModelsForAuthType(currentAuthType)` — an in-memory
registry seeded once at construction from the hardcoded QWEN_OAUTH_MODELS
(a single entry, `coder-model`) for qwen-oauth, and from the user's
`modelProviders` setting for every other authType. Under
`authType: "openai"` with no `modelProviders` declared it returns `[]`,
verified against the live SDK. qwen-code never asks the gateway what it
hosts, so there was nothing for codeoid to cache — `_cacheModels`
correctly ignores empty reports.

Ask the gateway directly instead. On the openai path it is an
OpenAI-compatible endpoint, so `GET /models` is authoritative and stays
current as the provider adds models — no hand-maintained list on any box.
The qwen-code registry is unioned in rather than replaced: it carries real
display labels, and qwen-oauth has a dynamic base URL and no key to
present, so HTTP is not an option there. A failed fetch falls back to the
registry; the session still runs, only the picker is poorer.

Everything the endpoint reports is surfaced. The response carries only
{id, object, created, owned_by} with no modality field, so filtering out
image/audio entries would mean pattern-matching ids — the same
hardcoded-list problem this removes.

Two supporting fixes:

- normalizeModelCatalog read `name` for the display label, but the SDK
  emits `label` (its handler projects the registry entry down to
  {id, label, capabilities, contextWindowSize}). Every model rendered as
  its raw id. `name`/`modelId`/`availableModels` stay as accepted aliases.

- _cacheModels was first-non-empty-wins per daemon lifetime. Providers
  report on each query-loop build, so a model added to a gateway now
  appears on the next session instead of after a daemon restart. Empty
  reports are still ignored so a failed fetch cannot clobber a good
  catalog, and an unchanged report skips the SQLite write.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
@saucam
saucam merged commit 73e1331 into main Sep 1, 2026
4 checks passed
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