Skip to content

fix(desktop): serve Claude OAuth models from the curated catalog and report real error causes - #2336

Merged
jackwener merged 4 commits into
apache:mainfrom
Joob1n:fix/claude-oauth-model-catalog
Aug 7, 2026
Merged

fix(desktop): serve Claude OAuth models from the curated catalog and report real error causes#2336
jackwener merged 4 commits into
apache:mainfrom
Joob1n:fix/claude-oauth-model-catalog

Conversation

@Joob1n

@Joob1n Joob1n commented Aug 6, 2026

Copy link
Copy Markdown
Contributor

Summary

Clicking 更新模型目录 on a Claude OAuth (claude-subscription) connection always fails, and the toast always blames the network. Two independent defects stack:

  1. The request can only ever 401. The subscription OAuth flow requests session-scoped tokens (user:sessions:claude_code user:mcp_servers user:file_upload — no user:inference), and GET api.anthropic.com/v1/models rejects those with 401. Verified against the live endpoint: the same request shape succeeds with a full-scope token and there is no session-scope path to the catalog.
  2. Every failure class renders as 网络错误. Electron wraps ipcMain.handle rejections as Error invoking remote method 'connections:fetchModels': …, and the renderer ran its keyword classifier over the whole wrapper — the fetch in the channel name matches the network branch, so auth failures, not-logged-in, timeouts, and rate limits all surfaced as network errors. This also masked defect 1 during diagnosis: fixing a genuinely-broken proxy changed the underlying error from ECONNREFUSED to 401 with no visible change in the toast.

Fixes, one commit each:

  • bugfix(desktop): providerPanelActionErrorMessage strips the IPC wrapper before classifying, translates known status strings through the shared lastTest map, passes display-ready (already localized, redacted) main-process copy through, and only then falls back to keyword classification.
  • bugfix(providers): the registry declares fallback-only discovery for claude-subscription (with the scope rationale recorded in reason), which hides the fetch button via the existing providerSupportsModelDiscovery gate and routes the picker to the curated catalog. Both static lists drop the retired claude-opus-4-1 generation and lead with the current 5-family (claude-opus-5, claude-sonnet-5); all ids resolve display names and context windows from the models.dev snapshot.

Chat sends are unaffected — they use the session-token transport, not /v1/models.

Verification

  • @maka/core tests: 793/793 pass. Updated the provider-auth contract test (fetch_models now correctly derives hidden for claude-subscription) and added a codex counter-case keeping the discovery-capable expectation covered.
  • @maka/desktop main tests: 1770/1770 pass, plus full four-project typecheck (renderer included). One catalog test fixture that used claude-subscription for same-provider disambiguation now uses openai-codex, semantics unchanged.
  • @maka/runtime tests: 3312 pass; the 14 failures are rg: command not found in builtin-tools Grep tests (ripgrep not installed on this machine, unrelated). The rewritten model-fetcher test asserts claude-subscription discovery serves the curated list without touching the network.
  • biome lint/format clean on all changed files.

Root cause

Live diagnosis on a real login: the stored credential's scope field is user:file_upload user:mcp_servers user:sessions:claude_code, and replaying the app's exact discovery request (undici + proxy dispatcher + subscription beta headers) against api.anthropic.com/v1/models succeeds with a full-scope token — isolating the 401 to token scope, not headers, proxy, or transport.

Joob1n added 2 commits August 6, 2026 21:32
…PC message

Electron wraps ipcMain.handle rejections as "Error invoking remote method
'<channel>': Error: <message>". providerPanelActionErrorMessage ran the
keyword classifier over that whole wrapper, so the "fetch" inside
'connections:fetchModels' matched the network branch and every failure of
更新模型目录 — auth, not-logged-in, timeout, rate limit — surfaced as 网络错误.
It also re-classified messages the main process had already localized,
flattening specific copy into the generic fallback.

Strip the IPC wrapper first, translate known status strings through the
shared lastTest map, pass display-ready Chinese copy through (redacted),
and only then fall back to keyword classification.
…list, not /v1/models

The subscription OAuth flow requests session-scoped tokens
(user:sessions:claude_code user:mcp_servers user:file_upload — no
user:inference), and api.anthropic.com/v1/models rejects those with 401.
更新模型目录 for this provider could therefore only ever fail, and the
failure surfaced as a misleading 网络错误 toast.

Declare fallback-only discovery in the registry so the fetch button hides
and the picker serves the curated catalog, and refresh both static lists:
drop the retired claude-opus-4-1 generation and lead with the current
5-family (claude-opus-5, claude-sonnet-5).
@Joob1n Joob1n changed the title bugfix: stop Claude OAuth model discovery from 401ing, and stop every discovery error from reading as a network error fix(desktop): serve Claude OAuth models from the curated catalog and report real error causes Aug 6, 2026
@Joob1n

Joob1n commented Aug 6, 2026

Copy link
Copy Markdown
Contributor Author

@likun666661 review plz

Joob1n added 2 commits August 6, 2026 21:49
Electron serializes a rejected handler as `${error.name}: ${error.message}`,
so a main-process ConnectionModelDiscoveryPreconditionError arrived as
"Error invoking remote method '…': ConnectionModelDiscoveryPreconditionError: …"
and the wrapper strip — anchored on the literal "Error: " — missed it,
leaking the whole English wrapper into the toast around the most common
precondition message (这个 OAuth 模型连接还没有登录).
…rovider

The claude-subscription rewrite dropped the only test exercising
anthropicV1Url's stored-/v1 normalization; re-pin it through the provider
that still discovers over the anthropic wire.
@jackwener
jackwener merged commit 434e122 into apache:main Aug 7, 2026
jackwener added a commit that referenced this pull request Aug 7, 2026
…OAuth

claude-subscription discovery is curated/fallback-only after #2336, so
beginModelFetch returns provider_action_unavailable. Seed the OAuth backend
abort test from the curated inventory create already writes, and target a
model id that inventory contains.
jackwener added a commit that referenced this pull request Aug 7, 2026
… OpenAI-compatible connections (#2463)

* feat(core,runtime-host,desktop): declare relay model profiles per model

A generic openai-compatible relay fronts models the built-in metadata chain
has never heard of: no thinking-level menu, no vision decision, a guessed
context window. This adds first-class, per-model user declarations for the
three facts only the user can decide.

Data model: `Connection.relayModelProfiles: Record<modelId,
{ thinkingLevels?; vision?; contextWindow? }>` on the single connection type
chain. Two invariants are enforced at every store boundary — profiles exist
only on openai-compatible connections (the metadata chain is the truth
elsewhere), and only for ids in `enabledModelIds` (both stores prune the
table when a model is disabled, so no declaration can outlive its row).
Every read enters through one gated seam, `relayModelProfile(connection,
modelId)`; writes are sanitized by `normalizeRelayModelProfiles`, and the
catalog codec validates strictly (closed level vocabulary, subset of the
selection, positive-integer context window) — a malformed legacy document is
noise, a malformed catalog document is a loud failure.

Updates are whole-table tri-state: an absent key leaves the stored table
untouched (capability-blind writers cannot clobber), `null` clears it, a
table replaces it. A baseUrl change in the same update retires an untouched
table unless the update submits one of its own — declarations belong to the
endpoint they were declared against. The embedded store mirrors the same
merge rules, and config import round-trips the typed field instead of an
extras bag.

Host wire: profiles travel on their own `enabled_model_id` page item
(`relayProfile`), never in one header table — a header item is atomic to the
paginator's 48 KiB page budget, so a long declaration list there would have
made the whole connection unreadable. The client reader reassembles the
per-model map; compatibility epoch bumps to 6. Session-model gating and
execution authority consume the same typed table, so a level that passes the
gate is exactly what the wire can send; the connection slug is the
provider-options namespace, and a fake-fetch test pins the translated
`reasoning_effort` request body.

Settings UI: each enabled model row edits a local draft (precise
multi-select of thinking levels — generic relays never declare the 'off'
wire —, Auto/Enabled/Disabled vision tri-state, positive-integer context
window) committed by one explicit 保存能力声明; there is no per-keystroke
persistence machinery. The add-provider form declares the connection's
first (default) model at create time through the same typed field.

* fix(storage): prune relay profiles against the full create-time selection

Create previously passed only defaultModel into relayProfilesForStorage, so
import/create payloads that seed enabledModelIds together with per-model
profiles dropped every non-default declaration. Use the same enabled set the
connection will store, and cover the multi-model create path.

* fix(runtime-host): stop opening model fetch for fallback-only Claude OAuth

claude-subscription discovery is curated/fallback-only after #2336, so
beginModelFetch returns provider_action_unavailable. Seed the OAuth backend
abort test from the curated inventory create already writes, and target a
model id that inventory contains.

---------

Co-authored-by: sans <sansm@mbot.local>

@Astro-Han Astro-Han left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks — the switch to fallback discovery is verified sound: zero network on runtime, the 401 root cause is real (the token scope can't call /v1/models), the curated-vs-registry lists currently agree, legacy defaults survive via connection_default provenance, and core + runtime + desktop targeted suites pass. Three P2s to consider:

  1. Two hand-maintained lists, no consistency constraint. curatedCatalogFallbackModelsForProvider (model-metadata.ts:559-566) and the registry fallbackModels (provider-registry.ts:1768-1776) are separate constants that must stay equal for picker and runtime to agree — nothing enforces it, and the model-fetcher test derives its expectation from the same constant it tests (self-referential). This becomes the amplification point when #2399's snapshot refresh lands. A contract test asserting equality (or merging to one source) would close it.
  2. The headline content (opus-5/sonnet-5 added) isn't pinned. No test asserts the six ids/order or that their metadata resolves. A literal pin in the catalog contract (ids + recommendedRank + lookupModelMetadata non-empty) would make a snapshot regeneration losing opus-5 fail loudly instead of drifting.
  3. The error-classification rewrite (2 of the 4 commits) has zero tests. cleanEventMessage and providerPanelActionErrorMessage have no references anywhere; the new CJK pass-through changes the display contract for all 10+ call sites in en locales (Chinese main-process messages now show verbatim), and the regex only matches one wrapper shape so a non-Error rejection could still misreport fetch failures as network errors. A small zh+en classification matrix test would pin it.

P3s: the anthropicModelHeaders OAuth branch is now dead code, the model-metadata.ts:117 comment is stale (opus-5 has been in the snapshot since #2050), the new default-model jump to opus-5 ignores subscription tier, and the context-window gap for OAuth models persists (displayMetadataOnly strips it — #2399 doesn't fix that either, noted). Approving.

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.

3 participants