3 months ago
9cbb272* feat: support custom provider model IDs
Add persistent custom model ID storage and merge configured entries into provider catalogs for OpenAI, Google, Anthropic, NVIDIA, Together, Fireworks, Baseten, MiniMax, OpenCode, Bedrock, and Antigravity providers.
Expose JSON-RPC add/delete controls, update generated protocol docs, and allow configured custom IDs through model metadata and runtime resolution with conservative fallbacks.
Refresh the desktop Models UI with inline custom model controls, compact available-model summaries, and selector-side custom ID entry for supported providers.
Tests: bun run docs:generate-jsonrpc; focused custom-model tests; bun run typecheck; bun run docs:check; bun run check; bun run lint; bun run test.
* test: harden Windows temp cleanup
* fix: preserve selected reasoning effort during hydration
Keep an explicit composer reasoning effort visible while stale session_config payloads arrive, and prefer live runtime reasoning metadata for active OpenAI-compatible threads before falling back to persisted provider options.
Add desktop reducer and ChatView regressions covering the XHigh-to-Medium snapback.
* fix: flatten assistant markdown table chrome
Disable Streamdown table controls in desktop markdown and flatten its table wrapper so assistant tables stay inside the message flow without extra card chrome.
Add a desktop markdown regression that verifies table controls are absent and wrapping classes are applied.
* feat: render mermaid diagrams and inline images in chat markdown
- Fix mermaid fences: PreWithCopy now forwards Streamdown's data-block
marker for language-mermaid so the diagram renderer engages (was
falling through to inline code); keep hover copy button for other
fences. Mermaid controls trimmed to fullscreen + pan/zoom and diagram
theme follows the app dark/light mode.
- Add cowork-media: Electron protocol serving local image files with an
image-extension allowlist, traversal normalization, and testable URL
encode/decode helpers shared between renderer and main.
- Rewrite markdown image sources (absolute paths, file:// URLs, and
workspace-relative paths via desktopBasePath) to cowork-media at the
remark stage; extend sanitize schema with img alt/title and the
cowork-media src protocol.
- Add DesktopMarkdownImage: constrained inline rendering with
click-to-preview for local files, confirm-and-open for remote URLs,
and a file-chip fallback when loading fails.
- Document inline images and mermaid diagrams in system prompts so the
model uses them for charts, screenshots, and architecture sketches.
Co-authored-by: Cursor <cursoragent@cursor.com>
* feat: add global model preferences store
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
* feat: apply model preferences to provider catalogs
Discovered models outside the curated registry default to disabled;
custom models and codex app-server models stay enabled. Overrides from
model-preferences.json win either way, with fail-open when curation and
discovery do not overlap and defaultModel repair when the default is
disabled.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
* feat: add provider model setEnabled/resetEnabled JSON-RPC controls
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
* feat: add per-provider Manage models UI with enable/disable checkboxes
Desktop settings gains a Manage models dialog per provider (search,
checkbox toggles, enable/disable all, reset to defaults, custom model
add/remove). Pickers across desktop and mobile hide models flagged
enabled:false.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
* chore: unexport internal model-preference symbols
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
* feat: curate default-enabled models for open-model providers
Together, NVIDIA, MiniMax, Baseten, Fireworks, Fire Pass, and OpenCode
catalogs now default to Nemotron 3 Ultra, MiniMax M3, GLM 5.2,
Kimi K2.6, DeepSeek V4 Pro, and DeepSeek V4 Flash (matching
provider-specific id spellings such as kimi-k2p6). Catalogs carrying
none of the curated set fall back to registry defaults so a provider
never goes dark.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
* Update CLAUDE.md
* fix: report custom-model and preference mutation outcomes to the UI
Await custom-model adds before selecting them in the composer, restore
the typed ID when an add fails, and roll back optimistic Manage models
checkbox state when the setEnabled RPC fails.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
* fix: resolve configured custom model IDs in sync resume and normalization
Persisted sessions no longer migrate a configured custom model to the
provider default on resume, and normalizeModelIdForProvider accepts a
configured custom ID even when it also exists in another provider's
registry.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
* fix: use exact PI lookups with conservative custom-model fallbacks
Unknown custom OpenAI and Anthropic IDs no longer inherit the first
catalog model's pricing/metadata; they resolve through dedicated
fallback builders with unknown cost and conservative limits.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
* fix: serialize global model store writes across processes
Custom-model and model-preference mutations now take a sidecar file
lock (mkdir + pid stale takeover, mirroring SessionDbWriteCoordinator)
so concurrent workspace servers cannot drop each other's writes.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
* fix: constrain chat media rendering to approved workspace roots
The cowork-media protocol now enforces the same resolveAllowedPath
boundary as the file IPC handlers, and markdown image resolution blocks
relative paths that escape the workspace base instead of letting them
resolve outside it.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
* fix: mark catalog models duplicating a custom ID as custom-managed
A custom ID that collides with a discovered or static catalog model
keeps its discovered metadata but carries the custom marker, so clients
can surface the store entry for removal instead of hiding it.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
* fix: compare pending reasoning effort against the draft composer provider and reconcile Manage models pending state
session_config no longer clears a pending reasoning effort using the
live config provider when the composer is showing a draft provider, and
the Manage models dialog reconciles optimistic checkbox state per model
instead of wiping all in-flight toggles on each catalog refresh.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
* fix: harden store lock and thread auth home into custom-model resume
- Never take over a store file lock while its owner process is alive; a
slow-but-live owner surfaces as an acquire timeout instead of a
takeover that would reopen the lost-update race.
- Resolve persisted-session custom models against the session auth home
(resolveAuthHomeDir(baseConfig)) so non-default homedirs are honored.
- Resume configured Bedrock custom IDs missing from the static/cache
snapshot as placeholders instead of migrating to the provider default.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
* fix: label disabled built-ins correctly and rewrite raw-HTML image srcs
- A selected-but-disabled built-in model no longer shows the '(custom)'
suffix reserved for user-added custom IDs in the composer picker.
- Raw HTML <img> tags with workspace-relative srcs are rewritten to
cowork-media at a new rehype stage (before harden) and resolved against
the workspace base path, so they render like markdown images while
escaping paths stay blocked.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
* chore: unexport internal rehypeRewriteDesktopImages
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
* fix: keep active disabled model visible on mobile and drop blocked image srcs
- Mobile model pickers keep the current default pill visible even when
that model is disabled, matching the desktop composer.
- DesktopMarkdownImage resolves via the tree resolver so a base-escaping
src is dropped ('') instead of falling back to the raw escaping path.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
* fix: clear pending toggles when resetting Manage models to defaults
Reset discards local intent, so it now clears optimistic pendingById
immediately; otherwise a still-in-flight toggle survived the reconcile
and left a stale checkbox after the server restored defaults.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
* fix: thread session auth home through sync model resolution and preserve discovered IDs on resume
- Thread resolveAuthHomeDir(config) through every sync
normalizeModelIdForProvider / child-routing caller (config.getModel,
getModelForProvider, modelRouter, ProviderAuthManager,
SessionMetadataManager), so a configured custom cross-registry ID is
accepted during selection on a non-default-homedir server instead of
being rejected as foreign.
- Resume now consults the discovery cache (new sync
readModelDiscoveryCacheSync + isDiscoveredModelIdSync), so a selected
newly-discovered model is preserved instead of migrating to the
provider default when the thread reopens.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
* fix: key the all-disabled hint off the enabled count including custom models
The 'All models are disabled' hint keyed off the standard-only preview
chips, so a provider with all built-ins disabled but custom models
enabled was mislabeled. It now keys off enabledModelCount (custom +
standard).
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
* fix: thread session auth home through remaining custom-model resolution paths
- Thread resolveAuthHomeDir(config) into resolvePiModel's sync metadata
lookups (applySupportedModelMetadata) so a configured custom
cross-registry ID resolves on the first turn under a non-default home.
- Pass the session home into mergeProviderOptionDefaults so config load
doesn't throw while merging provider defaults for a custom ID.
- The post-normalization child-model guard now keeps a custom-only
preferred child model (consulting the custom store), not just
discovered ones.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
* fix: thread session auth home through remaining runtime/prompt model lookups
Extends the auth-home threading to every remaining sync metadata caller
so a configured custom cross-registry ID resolves under a non-default
homedir instead of aborting the first turn:
- openaiResponsesModel and googleInteractionsModel runtimes
- resolveModelMetadata's allowPlaceholder (prompt-load) fall-through
- prompt.ts system-prompt loading + spawn-agent prompt body
- getKnownResolvedModelMetadata reads the Bedrock discovery snapshot from
the session home so a discovered Bedrock model resumes correctly
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
* fix: prefer confirmed config over stale runtime reasoning effort
The composer reasoning selector prioritized the runtime effort captured
at creation/resume (never refreshed on a config ack) over the confirmed
session config, so it snapped back to the old value after a change was
confirmed. Extracted resolveCurrentReasoningEffort with the corrected
precedence (composer -> config -> runtime -> default).
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
* fix: sync runtime reasoning effort on config ack instead of reordering precedence
The prior fix reordered the selector precedence (config before runtime),
which broke the intentional runtime-first behavior (the session may run
an effort that differs from a stale config). Revert to runtime-first and
instead keep the runtime effort in sync: when a pending composer effort
is confirmed by the session_config ack, update requested/effective
reasoning effort to the confirmed value so the selector no longer snaps
back. Guards against Google's 'dynamic' mode (openai/codex-cli only).
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
* fix: resolve a stuck pending reasoning effort when the server settles a different value
The optimistic composer effort was only cleared when the config ack
matched it exactly, so a server that clamped/rejected the change (or a
set_config that never sent) left the selector stuck showing the pending
value over runtime and config. Now clear it when the config settles any
value (matches our request, or changes a previously-defined effort to a
different one), sync the runtime fields to that authoritative value, and
revert the optimistic value if set_config fails to send. The lagging
case (config unchanged) still preserves the pending value.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
* fix: preserve discovered-model capabilities and drop stray reasoning defaults
- Resolve discovered models (sync resume + async path) from their cached
entry so a reopened session keeps supportsImageInput/displayName/
knowledgeCutoff instead of a generic placeholder that downgrades vision.
- getResolvedModelMetadataSync's Bedrock branch forwards opts.home so a
Bedrock model in the discovery snapshot under a non-default home
resolves with cached metadata.
- Custom placeholders no longer inherit the provider default's reasoning
option defaults (reasoningEffort/reasoningSummary) unless the id is
reasoning-capable, so a custom non-reasoning OpenAI id (gpt-4o) does not
send a reasoning payload its runtime fallback rejects.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
* fix: drop stale reasoning options when selecting a non-reasoning model
prepareModelSelection spreads the prior config's providerOptions into the
new config, so switching a live thread from a reasoning model (GPT-5) to
a non-reasoning custom id (gpt-4o) forwarded the old reasoningEffort/
reasoningSummary and the first OpenAI Responses request sent a reasoning
payload the fallback model rejects. Added reconcileReasoningProviderOptions
to strip reasoning keys the newly-resolved model does not declare in its
defaults; a reasoning-capable model keeps a user's explicit effort.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
* fix: preserve discovered capabilities, reconcile config reasoning, correlate catalog mutations, keep fail-open under overrides
- getResolvedModelMetadataSync consults the discovery cache before its
placeholder, so a runtime turn on a cache-only model keeps vision/
reasoning capabilities instead of downgrading to text-only.
- mergeProviderOptionDefaults reconciles reasoning options so a config
that retained reasoningEffort from a prior model does not reintroduce
it onto a custom non-reasoning id at load.
- Provider catalog mutation routes use captureWorkspaceControlMutationEvents
and return the last catalog, so the JSON-RPC call cannot settle on a
concurrent refresh's catalog before the store write completes.
- applyModelPreferencesToCatalogEntry keeps its fail-open baseline under
a single override, so disabling one model in an all-unknown catalog
hides only that model, not the whole catalog.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
* fix: strip GPT-5-only verbosity for non-GPT-5 ids and stop inheriting reasoning for non-reasoning children
- textVerbosity is a GPT-5-family-only parameter, so a custom/discovered
non-GPT-5 OpenAI id (gpt-4o, o3) no longer carries it in its placeholder
defaults; reconciliation uses the combined model-gated key set so stale
verbosity is dropped on switch/load too. o-series keeps reasoning but not
verbosity.
- modelRouter no longer falls through to the parent's reasoning effort when
a child is routed to a different, non-reasoning model: a changed child
uses its own default effort (undefined for non-reasoning), only an
unchanged child inherits the parent effort.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
* fix: clear stale runtime reasoning effort when settled config carries none
When a session_config ack cleared an optimistic composer effort but the
incoming config had no reasoningEffort, the runtime fields were left
untouched. With runtime-first selector precedence the composer kept
showing the previous turn's effort instead of the settled config/default.
Distinguish "leave runtime alone" from "clear runtime to null" via an
explicit syncRuntime flag so openai/codex-cli threads always sync their
runtime effort to the authoritative config value.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
* fix: reconcile child reasoning options, gate images on resolved metadata, outwait store locks
Addresses three P2 review findings on PR #198:
- modelRouter: routing a reasoning parent (GPT-5 with reasoningEffort/
reasoningSummary) to a non-reasoning child (custom/discovered gpt-4o) left
the stale reasoning keys in the child config because applyReasoningEffort
returns the copied parent options unchanged. Reconcile against the child
model's resolved defaults so it drops keys the model does not declare.
- image gates: attachment materialization and the read tool gated visual
content on the static-registry-only supportsImageInput, so a discovered
vision model advertised in the catalog/runtime was downgraded to text-only.
Route both through a new modelSupportsImageInputSync that resolves the same
dynamic metadata (registry -> discovery cache -> custom store).
- provider mutations: custom-model/model-preference writes wait up to the
file-lock acquire timeout (10s) for the cross-process store lock, but the
JSON-RPC mutation capture timed out at 5s and dropped the catalog the store
later emitted, leaving the UI showing stale state. Pass a capture timeout
that outwaits the store lock plus a write/emit margin.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
* fix: make file-lock ownership atomic and prefer discovered over custom metadata
Addresses two P2 review findings on PR #198:
- fileLock: the stale-lock reclaim removed an ownerless lock directory based on
mtime, so a creator paused between mkdir and its owner write could have its
directory stolen and then, on resume, overwrite the new owner and re-enter the
critical section — reopening the lost-update race on the model stores. Make the
owner.json write the atomic mutex (exclusive wx/O_EXCL create): whoever wins
the owner file owns the lock, an orphaned directory is claimed without an
mtime reclaim, and a paused creator whose lock was claimed can no longer
clobber it. Added a deterministic regression test via a new onBeforeOwnerWrite
test seam.
- metadata: for an id present in both the custom store and the discovery cache,
strict selection (resolveModelMetadata) and resume (getKnownResolvedModelMetadata)
returned the generic custom placeholder before consulting the discovery cache,
losing the cached display/vision/reasoning metadata the catalog advertises for
custom-managed discovered models. Prefer the discovered entry over the custom
placeholder in both paths.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
* fix: reasoning-metadata fidelity for custom and discovered models
Addresses three P2 review findings on PR #198:
- catalog: a custom-only OpenAI reasoning id (e.g. o3-preview-custom or a gpt-5
deployment) produced a catalog entry with no reasoning block, so desktop
reasoningConfigFromCatalog() (which only falls back to static-registry models)
rendered no reasoning selector even though the runtime treats it as reasoning-
capable. Derive the entry's reasoning block from the same placeholder
resolution model selection uses.
- metadata: buildDiscoveredModelMetadata reduced the cache's reasoning info to a
boolean, so a discovery entry advertising a specific defaultEffort still got
the provider fallback (e.g. OpenAI "high") in providerOptionsDefaults, which
config loading and child routing then send. Honor the cached default effort.
- metadata: customModelIdLikelySupportsReasoning returned true for non-OpenAI
providers, so a user-added non-reasoning custom id (e.g. a legacy claude-3-5
deployment) inherited the provider default thinking/effort keys and failed the
first turn with unsupported thinking params. Default unproven custom ids to
non-reasoning so those keys are stripped; static/discovered models that
advertise reasoning pass supportsReasoning explicitly and are unaffected.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
---------
Co-authored-by: Cursor <cursoragent@cursor.com>
Co-authored-by: Claude Fable 5 <noreply@anthropic.com>Parentbb2ea88