-
Notifications
You must be signed in to change notification settings - Fork 90
fix(macos): atomic provider+model save via single PATCH #26156
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -2983,6 +2983,13 @@ public final class SettingsStore: ObservableObject { | |
| /// the unified `llm.default.provider` key. This is the canonical write | ||
| /// path now that the workspace migration consolidates LLM call-site | ||
| /// settings under `llm.*` (see PR 4 of the unify-llm-callsites plan). | ||
| /// | ||
| /// Prefer `setLLMDefault(provider:model:)` when both keys change together | ||
| /// — it writes them atomically in a single PATCH so the daemon's | ||
| /// `ConfigWatcher` cannot observe a half-applied state with the new | ||
| /// provider but the old (potentially incompatible) model. This single-key | ||
| /// variant is kept for future flows that legitimately want to change just | ||
| /// the provider without touching the model. | ||
| @discardableResult | ||
| func setLLMDefaultProvider(_ provider: String) -> Task<Bool, Never> { | ||
| selectedInferenceProvider = provider | ||
|
|
@@ -3002,6 +3009,14 @@ public final class SettingsStore: ObservableObject { | |
| /// Persists the default LLM provider+model pair under `llm.default`. | ||
| /// Both keys are written together so the daemon's read-modify-write cycle | ||
| /// observes a consistent pair. | ||
| /// | ||
| /// Prefer `setLLMDefault(provider:model:)` when both keys are being | ||
| /// changed in response to a single user action — it skips the dedup | ||
| /// gating below and writes a clean atomic PATCH. This entry point is | ||
| /// retained for flows that want the dedup-aware "only patch if the | ||
| /// resolved pair actually moved" semantics, e.g. routing-source refresh | ||
| /// pushing the daemon's authoritative selection back through the same | ||
| /// helper. | ||
| @discardableResult | ||
| func setLLMDefaultModel( | ||
| _ model: String, | ||
|
|
@@ -3036,6 +3051,39 @@ public final class SettingsStore: ObservableObject { | |
| return task | ||
| } | ||
|
Comment on lines
3051
to
3052
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🚩 setLLMDefaultProvider and setLLMDefaultModel have zero production callers after this PR After this PR, (Refers to lines 2994-3052) Was this helpful? React with 👍 or 👎 to provide feedback. |
||
|
|
||
| /// Persists the default LLM provider+model pair atomically in a single | ||
| /// `llm.default` PATCH. Use this whenever both keys are being changed in | ||
| /// response to a single user action (e.g. the inference settings save). | ||
| /// | ||
| /// Splitting the write into two PATCH requests (provider first, model | ||
| /// second) gives the daemon's `ConfigWatcher` a window to fire on the | ||
| /// provider-only change and reload providers with the OLD model — which | ||
| /// may not exist in the new provider's catalog (Anthropic provider trying | ||
| /// to use an OpenAI model ID, etc.). Writing both keys in one PATCH | ||
| /// closes that window. | ||
| @discardableResult | ||
| func setLLMDefault(provider: String, model: String) -> Task<Bool, Never> { | ||
| lastDaemonModel = model | ||
| lastDaemonProvider = provider | ||
| selectedModel = model | ||
| selectedInferenceProvider = provider | ||
| let task = Task { | ||
| let success = await settingsClient.patchConfig([ | ||
| "llm": ["default": ["provider": provider, "model": model]] | ||
| ]) | ||
| if !success { | ||
| log.error("Failed to patch config for llm.default.{provider,model}") | ||
| if lastDaemonModel == model { | ||
| lastDaemonModel = nil | ||
| lastDaemonProvider = nil | ||
| } | ||
| } | ||
| return success | ||
| } | ||
| scheduleRoutingSourceRefresh() | ||
| return task | ||
| } | ||
|
|
||
| // MARK: - Per-Call-Site Override Read / Write | ||
|
|
||
| /// Number of entries in `callSiteOverrides` that have at least one | ||
|
|
||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Launching
setInferenceModewithout awaiting it allows the mode PATCH to race with the newsetLLMDefault(provider:model:)PATCH whenever the user toggles mode and saves. The runtime config PATCH handler does aloadRawConfig→ merge →saveRawConfigcycle, so concurrent requests can clobber each other’s keys; if the mode request finishes last, it can restore the oldllm.default, and if the llm request finishes last, it can restore the oldservices.inference.mode. This makes saves nondeterministic in exactly the mode-switch flow this change touches.Useful? React with 👍 / 👎.