patch: preserve Claude Desktop model overrides (#2088)
Some checks are pending
Post merge / Warm dependency cache (Linux) (push) Waiting to run
Post merge / Warm dependency cache (Windows) (push) Waiting to run
Post merge / Warm dependency cache (macOS) (push) Waiting to run
Post merge / Publish to PyPI (push) Waiting to run

## Why

FCC deletes Claude Desktop's saved model override during refresh,
startup, and Connect. It also reports an otherwise valid gateway profile
as disconnected whenever that override is present.

Fixes #2085.

## How

Preserve `inferenceModels`, including its order and per-model metadata,
when updating an existing FCC profile. Determine connection status from
the gateway settings without requiring the override to be absent.

Add regression coverage for refresh, Connect, credential rotation,
startup, and read-only Admin status. Fresh profiles use discovery, and
users can remove their override in Claude Desktop.

Local CI passed in a clean worktree: 9,217 Python tests and 277 browser
tests, plus formatting, lint, typing, and suppression checks.


<!-- greptile_comment -->

<!-- greptile_summary -->

<h2><a
href="https://app.greptile.com/api/retrigger?id=81273218"><picture><source
media="(prefers-color-scheme: dark)"
srcset="https://greptile-static-assets.s3.amazonaws.com/badges/RetriggerDark.svg?v=2"><source
media="(prefers-color-scheme: light)"
srcset="https://greptile-static-assets.s3.amazonaws.com/badges/Retrigger.svg?v=2"><img
alt="Retrigger"
src="https://greptile-static-assets.s3.amazonaws.com/badges/Retrigger.svg?v=2"
align="right"></picture></a><a
href="https://app.greptile.com/alishahryar1/github/Alishahryar1/free-claude-code/-/pull/2088"><picture><source
media="(prefers-color-scheme: dark)"
srcset="https://greptile-static-assets.s3.amazonaws.com/badges/ViewInGreptileDark.svg?v=2"><source
media="(prefers-color-scheme: light)"
srcset="https://greptile-static-assets.s3.amazonaws.com/badges/ViewInGreptile.svg?v=2"><img
alt="View in Greptile"
src="https://greptile-static-assets.s3.amazonaws.com/badges/ViewInGreptile.svg?v=2"
align="right"></picture></a>Confidence Score: 5/5</h2>

<!-- greptile-impact-verdict --> Safe to merge based on the scoped
checks. Saved model overrides are preserved while gateway updates,
connection checks, and disconnect behavior continue to work.

<details open><summary><h3>Summary</h3></summary>

Preserves Claude Desktop's saved `inferenceModels`, including their
order and per-model metadata, when FCC updates an existing profile.
Saved overrides no longer make an otherwise valid connection appear
disconnected.

- Claude Desktop keeps saved model choices while FCC updates its gateway
profile.
</details>

<!-- greptile_confidence_score:5 -->

<sub>Reviews (1) · Last reviewed commit: ["Preserve Claude Desktop model
overrides"](c1ae11e2c9)
· [Reviewed by
Greptile](https://www.greptile.com/?utm_source=greptile_review&utm_medium=github&utm_campaign=review_public&utm_content=alishahryar1/free-claude-code)</sub>

<!-- /greptile_comment -->
This commit is contained in:
Ali Khokhar 2026-10-14 23:29:19 -07:00 • committed by GitHub
parent 261ecf7a18
commit bd984e28d3
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
4 changed files with 80 additions and 4 deletions

View file

@ -279,7 +279,6 @@ def _current(library: _Library, values: JsonObject) -> bool:
profile.get("inferenceGatewayBaseUrl"),
str(values["inferenceGatewayBaseUrl"]),
)
and "inferenceModels" not in profile
)
@ -293,7 +292,6 @@ def _update_profile(library: _Library, values: JsonObject) -> bool:
# Desktop otherwise restricts tool network access to the inference gateway.
profile.setdefault("coworkEgressAllowedHosts", ["*"])
profile["inferenceCustomHeaders"] = headers
profile.pop("inferenceModels", None)
return _write(library.profile_path, profile)

View file

@ -80,8 +80,15 @@ def test_desktop_routes_connect_disconnect_and_hide_credentials(client):
json.loads(profile.read_text())["inferenceGatewayBaseUrl"]
== "http://127.0.0.1:4321"
)
config = json.loads(profile.read_text())
models = [desktop_model_id("nvidia_nim/one")]
config["inferenceModels"] = models
profile.write_text(json.dumps(config))
before = {path: path.stat().st_mtime_ns for path in root.rglob("*.json")}
assert client.get(ROOT).json()["connected"] is True
response = client.get(ROOT)
assert response.json()["connected"] is True
assert "desktop-secret" not in response.text
assert json.loads(profile.read_text())["inferenceModels"] == models
assert before == {path: path.stat().st_mtime_ns for path in root.rglob("*.json")}
assert client.post(ROOT + "/disconnect").json()["connected"] is False
assert not profile.exists()

View file

@ -6,6 +6,7 @@ from types import SimpleNamespace
import pytest
from free_claude_code.config.paths import claude_desktop_disconnect_path
from free_claude_code.core.gateway_model_ids import desktop_model_id
from free_claude_code.harnesses import claude_desktop_integration as desktop
from free_claude_code.harnesses.claude_desktop_integration import (
check_unmanaged,
@ -170,6 +171,8 @@ def test_fresh_connection_and_disconnect_preserve_native_library(tmp_path):
assert read(meta(root))["appliedId"] == desktop.FCC_ID
assert read(mode(root))["deploymentMode"] == "3p"
assert read(profile(root))["inferenceGatewayApiKey"] == TOKEN
assert read(profile(root))["modelDiscoveryEnabled"] is True
assert "inferenceModels" not in read(profile(root))
assert read(profile(root))["coworkEgressAllowedHosts"] == ["*"]
before = {p: p.read_bytes() for p in root.rglob("*.json")}
assert (
@ -237,16 +240,71 @@ def test_connection_defaults_missing_egress_and_preserves_explicit_policy(
assert not desktop.refresh_connected(tmp_path, URL, TOKEN, disconnect_path=record)
@pytest.mark.parametrize("reconnect", [False, True])
@pytest.mark.parametrize(
"models",
[
[desktop_model_id("nvidia_nim/one"), desktop_model_id("nvidia_nim/two")],
[
{
"name": desktop_model_id("nvidia_nim/two"),
"labelOverride": "Preferred model",
"maxEffort": "high",
},
{"name": desktop_model_id("nvidia_nim/one")},
],
],
)
def test_model_override_survives_refresh_and_connect(tmp_path, reconnect, models):
record = claude_desktop_disconnect_path()
desktop.configure(tmp_path, URL, TOKEN, True, disconnect_path=record)
config = read(profile(tmp_path))
config["inferenceModels"] = models
write(profile(tmp_path), config)
before = profile(tmp_path).read_bytes(), profile(tmp_path).stat().st_mtime_ns
assert desktop.configure(tmp_path, URL, TOKEN, disconnect_path=record)["connected"]
if reconnect:
assert desktop.configure(tmp_path, URL, TOKEN, True, disconnect_path=record)[
"connected"
]
else:
assert not desktop.refresh_connected(
tmp_path, URL, TOKEN, disconnect_path=record
)
assert read(profile(tmp_path))["inferenceModels"] == models
assert (
profile(tmp_path).read_bytes(),
profile(tmp_path).stat().st_mtime_ns,
) == before
config.pop("inferenceModels")
write(profile(tmp_path), config)
assert not desktop.refresh_connected(tmp_path, URL, TOKEN, disconnect_path=record)
assert "inferenceModels" not in read(profile(tmp_path))
def test_refresh_rotates_credentials_without_reactivating(tmp_path):
desktop.configure(
tmp_path, URL, TOKEN, True, disconnect_path=claude_desktop_disconnect_path()
)
config = read(profile(tmp_path))
models = [
{"name": desktop_model_id("nvidia_nim/two"), "labelOverride": "Preferred"},
{"name": desktop_model_id("nvidia_nim/one"), "maxEffort": "high"},
]
config["inferenceModels"] = models
write(profile(tmp_path), config)
assert desktop.refresh_connected(
tmp_path,
"http://localhost:9000",
"new-token",
disconnect_path=claude_desktop_disconnect_path(),
)
refreshed = read(profile(tmp_path))
assert refreshed["inferenceModels"] == models
assert refreshed["inferenceGatewayBaseUrl"] == "http://localhost:9000"
assert refreshed["inferenceGatewayApiKey"] == "new-token"
assert desktop.configure(
tmp_path,
"http://localhost:9000",
@ -382,7 +440,7 @@ def test_other_profiles_and_unknown_settings_are_preserved(tmp_path):
)
assert read(profile(tmp_path))["keep"] is True
assert read(profile(tmp_path))["inferenceCustomHeaders"]["X-Other"] == "value"
assert "inferenceModels" not in read(profile(tmp_path))
assert read(profile(tmp_path))["inferenceModels"] == config["inferenceModels"]
metadata = read(meta(tmp_path))
metadata["appliedId"] = other
metadata["hybridPointer"] = {"bootstrapUrl": "https://example.test"}

View file

@ -15,6 +15,7 @@ from free_claude_code.config.api_keys import normalize_api_keys
from free_claude_code.config.loader import ManagedConfigStore
from free_claude_code.config.paths import claude_desktop_disconnect_path
from free_claude_code.config.settings import Settings
from free_claude_code.core.gateway_model_ids import desktop_model_id
from free_claude_code.harnesses import (
claude_desktop_integration as desktop,
)
@ -377,9 +378,21 @@ async def test_desktop_startup_rotates_selected_profile_and_disconnect_stays_off
True,
disconnect_path=claude_desktop_disconnect_path(),
)
profile = root / "configLibrary" / f"{desktop.FCC_ID}.json"
config = json.loads(profile.read_text())
models = [
{"name": desktop_model_id("nvidia_nim/one"), "labelOverride": "Pinned model"},
{"name": desktop_model_id("nvidia_nim/two"), "maxEffort": "high"},
]
config["inferenceModels"] = models
profile.write_text(json.dumps(config))
try:
await runtime.start()
await runtime._integrations._desktop_update.task
refreshed = json.loads(profile.read_text())
assert refreshed["inferenceModels"] == models
assert refreshed["inferenceGatewayApiKey"] == TOKEN
assert refreshed["inferenceGatewayBaseUrl"] == "http://127.0.0.1:8000"
result = await runtime.claude_desktop_status()
assert result["connected"] is True
assert result["update"]["changed"] is True