From f04b2e9fe8c06d871829c0be7cafae7c9c744685 Mon Sep 17 00:00:00 2001 From: James Wade Date: Fri, 22 May 2026 08:22:09 -0400 Subject: [PATCH] Pin Pi defaultProvider/defaultModel in settings.json MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Pi inherits the full environment from ucode, including any env vars that its pi-ai package recognizes as auth for a built-in provider (e.g. HF_TOKEN → huggingface). Pi's findInitialModel then picks the built-in before reaching our databricks-claude provider, routes to the wrong host, and validation 401s. Write settings.json alongside models.json so findInitialModel uses our provider/model directly. Fixes #68. Co-Authored-By: Claude Opus 4.7 (1M context) --- src/ucode/agents/pi.py | 14 ++++++++++++++ tests/test_agent_pi.py | 26 ++++++++++++++++++++++---- 2 files changed, 36 insertions(+), 4 deletions(-) diff --git a/src/ucode/agents/pi.py b/src/ucode/agents/pi.py index 995c1a5..69c7299 100644 --- a/src/ucode/agents/pi.py +++ b/src/ucode/agents/pi.py @@ -52,6 +52,7 @@ PI_UCODE_HOME = APP_DIR / "pi-home" PI_CONFIG_DIR = PI_UCODE_HOME / ".pi" / "agent" PI_CONFIG_PATH = PI_CONFIG_DIR / "models.json" +PI_SETTINGS_PATH = PI_CONFIG_DIR / "settings.json" PI_BACKUP_PATH = APP_DIR / "pi-models.backup.json" SPEC: ToolSpec = { @@ -182,11 +183,24 @@ def write_tool_config( providers.pop(stale, None) merged = deep_merge_dict(existing, overlay) write_json_file(PI_CONFIG_PATH, merged) + _write_settings(overlay["model"]) state = mark_tool_managed(state, "pi", managed_keys) save_state(state) return state, token +def _write_settings(model_selector: str) -> None: + # Pin defaultProvider/defaultModel in settings.json so Pi doesn't fall + # through to an env-key-backed provider (e.g. HF_TOKEN exposing + # huggingface) in `findInitialModel` when no --model is passed. + provider, _, model_id = model_selector.partition("/") + if not model_id: + return + existing = read_json_safe(PI_SETTINGS_PATH) + merged = deep_merge_dict(existing, {"defaultProvider": provider, "defaultModel": model_id}) + write_json_file(PI_SETTINGS_PATH, merged) + + def default_model(state: dict) -> str | None: """Prefer Claude opus → sonnet → haiku; fall back to codex, gemini.""" claude_models = state.get("claude_models") or {} diff --git a/tests/test_agent_pi.py b/tests/test_agent_pi.py index 02ec6f1..11d11e7 100644 --- a/tests/test_agent_pi.py +++ b/tests/test_agent_pi.py @@ -267,9 +267,11 @@ def _setup(self, tmp_path, monkeypatch): monkeypatch.setattr(config_io_mod, "APP_DIR", tmp_path) config_file = tmp_path / "models.json" backup_file = tmp_path / "pi-backup.json" + settings_file = tmp_path / "settings.json" monkeypatch.setattr(pi_mod, "PI_CONFIG_PATH", config_file) + monkeypatch.setattr(pi_mod, "PI_SETTINGS_PATH", settings_file) monkeypatch.setattr(pi_mod, "PI_BACKUP_PATH", backup_file) - return pi_mod, config_file + return pi_mod, config_file, settings_file def _state(self, **overrides) -> dict: state = { @@ -284,7 +286,7 @@ def _state(self, **overrides) -> dict: return state def test_stale_managed_providers_removed_before_merge(self, tmp_path, monkeypatch): - pi_mod, config_file = self._setup(tmp_path, monkeypatch) + pi_mod, config_file, _ = self._setup(tmp_path, monkeypatch) stale = { "providers": { @@ -312,7 +314,7 @@ def test_legacy_providers_removed_on_upgrade(self, tmp_path, monkeypatch): """Earlier ucode versions wrote `databricks-anthropic`, `databricks-codex`, and `databricks-oss` providers. They must be stripped on the next write so users don't end up with stale entries pointing at routes that 400.""" - pi_mod, config_file = self._setup(tmp_path, monkeypatch) + pi_mod, config_file, _ = self._setup(tmp_path, monkeypatch) config_file.write_text( json.dumps( @@ -339,7 +341,7 @@ def test_legacy_providers_removed_on_upgrade(self, tmp_path, monkeypatch): assert "databricks-claude" in written_providers def test_config_written_with_correct_model_and_token(self, tmp_path, monkeypatch): - pi_mod, config_file = self._setup(tmp_path, monkeypatch) + pi_mod, config_file, _ = self._setup(tmp_path, monkeypatch) with ( patch("ucode.agents.pi.get_databricks_token", return_value="tok"), @@ -350,3 +352,19 @@ def test_config_written_with_correct_model_and_token(self, tmp_path, monkeypatch written = json.loads(config_file.read_text()) assert written["model"] == "databricks-claude/claude-sonnet" assert written["providers"]["databricks-claude"]["apiKey"] == "tok" + + def test_settings_pins_default_provider_and_model(self, tmp_path, monkeypatch): + # Without this, Pi's `findInitialModel` can fall through to a built-in + # provider when an unrelated env var (e.g. HF_TOKEN) makes one look + # auth-configured. Pinning the default keeps Pi on our provider. + pi_mod, _, settings_file = self._setup(tmp_path, monkeypatch) + + with ( + patch("ucode.agents.pi.get_databricks_token", return_value="tok"), + patch("ucode.agents.pi.save_state"), + ): + pi_mod.write_tool_config(self._state(), "claude-sonnet", token="tok") + + settings = json.loads(settings_file.read_text()) + assert settings["defaultProvider"] == "databricks-claude" + assert settings["defaultModel"] == "claude-sonnet"