fix(desktop): serve Claude OAuth models from the curated catalog and report real error causes - #2336
Merged
Merged
Conversation
…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).
Contributor
Author
|
@likun666661 review plz |
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
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
approved these changes
Aug 7, 2026
Astro-Han
left a comment
Contributor
There was a problem hiding this comment.
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:
- 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.
- 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.
- 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.
4 tasks
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
Clicking 更新模型目录 on a Claude OAuth (claude-subscription) connection always fails, and the toast always blames the network. Two independent defects stack:
user:sessions:claude_code user:mcp_servers user:file_upload— nouser:inference), andGET api.anthropic.com/v1/modelsrejects 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.ipcMain.handlerejections asError invoking remote method 'connections:fetchModels': …, and the renderer ran its keyword classifier over the whole wrapper — thefetchin 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:
providerPanelActionErrorMessagestrips the IPC wrapper before classifying, translates known status strings through the sharedlastTestmap, passes display-ready (already localized, redacted) main-process copy through, and only then falls back to keyword classification.reason), which hides the fetch button via the existingproviderSupportsModelDiscoverygate and routes the picker to the curated catalog. Both static lists drop the retiredclaude-opus-4-1generation 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/coretests: 793/793 pass. Updated the provider-auth contract test (fetch_modelsnow correctly deriveshiddenfor claude-subscription) and added a codex counter-case keeping the discovery-capable expectation covered.@maka/desktopmain 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/runtimetests: 3312 pass; the 14 failures arerg: command not foundin 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/formatclean 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) againstapi.anthropic.com/v1/modelssucceeds with a full-scope token — isolating the 401 to token scope, not headers, proxy, or transport.