From e2dab7b72ec59f5844daecd47ecfbeb3e702d257 Mon Sep 17 00:00:00 2001 From: Andy Xu Date: Fri, 18 Sep 2026 21:39:21 +0000 Subject: [PATCH 1/5] Propagate managed Claude defaults for UC discovery --- src/ucode/agents/claude.py | 26 +++++++++++------ src/ucode/cli.py | 4 +++ tests/test_agent_claude.py | 59 ++++++++++++++++++++++++++++++++++++++ tests/test_cli.py | 48 +++++++++++++++++++++++++++++-- 4 files changed, 126 insertions(+), 11 deletions(-) diff --git a/src/ucode/agents/claude.py b/src/ucode/agents/claude.py index 341fce33..088f6e17 100644 --- a/src/ucode/agents/claude.py +++ b/src/ucode/agents/claude.py @@ -980,14 +980,21 @@ def _compose( if isinstance(last_applied_env.get(key), str) } for family, key in CLAUDE_DEFAULT_MODEL_ENV_KEYS.items(): - selected_default_model = _enforce_model_default_hierarchy( - family, - coding_agent_config_defaults=configured_defaults, - settings_file_existing_defaults=settings_file_existing_defaults, - ucode_defaults=ucode_defaults, - ucode_last_written_defaults=ucode_last_written_defaults, - enforced_models=enforced_models, - ) + if parent_schema is not None: + # A parent schema gets only admin-authored family mappings, never fallback + # defaults from local state or discovery. + selected_default_model = configured_defaults.get(family) + if selected_default_model and family in ("opus", "sonnet"): + selected_default_model = _maybe_add_1m_suffix(selected_default_model) + else: + selected_default_model = _enforce_model_default_hierarchy( + family, + coding_agent_config_defaults=configured_defaults, + settings_file_existing_defaults=settings_file_existing_defaults, + ucode_defaults=ucode_defaults, + ucode_last_written_defaults=ucode_last_written_defaults, + enforced_models=enforced_models, + ) if selected_default_model is None: target_env.pop(key, None) else: @@ -1055,7 +1062,8 @@ def _compose( state, lambda base: _compose( base, - enforce_model_default_hierarchy=provider is None and parent_schema is None, + enforce_model_default_hierarchy=provider is None + and (parent_schema is None or bool(coding_agent_config_defaults)), managed_settings_snapshots=managed_snapshots, ), managed_file_keys, diff --git a/src/ucode/cli.py b/src/ucode/cli.py index b59bb299..eab37538 100644 --- a/src/ucode/cli.py +++ b/src/ucode/cli.py @@ -2715,6 +2715,10 @@ def _launch_tool( # managed UC discovery likewise lets the agent select from the parent schema. Skip model # resolution, which would otherwise fail when global discovery found no models. resolved_model = None + if tool == "claude" and managed_parent_schema: + # Native discovery supplies the catalog, but the managed policy still controls + # which model Claude starts on. + route_root_model = managed_launch_model(managed or {}, recommendation, tool) if provider and tool == "claude" and (model or provider_models): if relayed: # Resolve against a curated allowlist so the forwarded id is one the gateway diff --git a/tests/test_agent_claude.py b/tests/test_agent_claude.py index c08f76a2..d80ed9a8 100644 --- a/tests/test_agent_claude.py +++ b/tests/test_agent_claude.py @@ -1079,6 +1079,65 @@ def test_managed_file_omits_workspace_defaults_for_parent_schema(self, monkeypat env = json.loads(managed_writes[0][1])["env"] assert not set(claude.CLAUDE_DEFAULT_MODEL_ENV_KEYS.values()) & env.keys() + def test_managed_file_applies_only_configured_defaults_for_parent_schema(self, monkeypatch): + private_writes: list = [] + managed_writes: list = [] + existing = { + str(FAKE_MANAGED_PATH): { + "env": {"ANTHROPIC_DEFAULT_HAIKU_MODEL": "system.ai.claude-haiku-4-5"} + } + } + self._patch(monkeypatch, private_writes, managed_writes, existing) + state = { + "workspace": WS, + "claude_models": { + "opus": "developer.claude-opus-4-8", + "sonnet": "developer.claude-sonnet-4-6", + }, + } + + claude.write_tool_config( + state, + None, + coding_agent_config_defaults={"opus": "system.ai.claude-opus-5"}, + parent_schema="system.ai", + ) + + env = json.loads(managed_writes[0][1])["env"] + assert env["ANTHROPIC_DEFAULT_OPUS_MODEL"] == "system.ai.claude-opus-5[1m]" + assert "ANTHROPIC_DEFAULT_SONNET_MODEL" not in env + assert "ANTHROPIC_DEFAULT_HAIKU_MODEL" not in env + + def test_managed_file_applies_all_configured_defaults_for_parent_schema(self, monkeypatch): + private_writes: list = [] + managed_writes: list = [] + self._patch(monkeypatch, private_writes, managed_writes) + defaults = { + "fable": "system.ai.claude-fable-5", + "opus": "system.ai.claude-opus-5", + "sonnet": "system.ai.claude-sonnet-5", + "haiku": "system.ai.claude-haiku-4-5", + } + + claude.write_tool_config( + {"workspace": WS, "claude_models": defaults}, + None, + route_root_model="system.ai.claude-sonnet-5", + coding_agent_config_defaults=defaults, + parent_schema="system.ai", + ) + + env = json.loads(managed_writes[0][1])["env"] + assert env["ANTHROPIC_MODEL"] == "system.ai.claude-sonnet-5" + assert { + family: env[key] for family, key in claude.CLAUDE_DEFAULT_MODEL_ENV_KEYS.items() + } == { + "fable": "system.ai.claude-fable-5", + "opus": "system.ai.claude-opus-5[1m]", + "sonnet": "system.ai.claude-sonnet-5[1m]", + "haiku": "system.ai.claude-haiku-4-5", + } + @pytest.mark.parametrize("with_catalog", [False, True]) def test_parent_schema_prunes_previous_static_picker(self, monkeypatch, with_catalog): private_writes: list = [] diff --git a/tests/test_cli.py b/tests/test_cli.py index 4a415b58..0d695033 100644 --- a/tests/test_cli.py +++ b/tests/test_cli.py @@ -4805,14 +4805,16 @@ def test_the_recommended_agent_gets_the_recommended_model(self, monkeypatch): ) assert cfg.call_args.args[2] == "system.ai.claude-haiku-4-5" - def test_passes_configured_claude_defaults_to_writer(self, monkeypatch): + def test_managed_parent_schema_passes_configured_claude_defaults_to_writer(self, monkeypatch): managed = { "enabled_agents": { "claude": { "model_config": { + "unity_catalog_location": "system.ai", + "default_model": "system.ai.claude-sonnet-4-6", "default_models_by_model_family": { "default_sonnet_model": "system.ai.claude-sonnet-4-6", - } + }, } } } @@ -4821,10 +4823,52 @@ def test_passes_configured_claude_defaults_to_writer(self, monkeypatch): result, _calls, cfg = self._launch(monkeypatch, managed=managed) assert result.exit_code == 0, result.output + assert cfg.call_args.kwargs["parent_schema"] == "system.ai" + assert cfg.call_args.kwargs["route_root_model"] == "system.ai.claude-sonnet-4-6" assert cfg.call_args.kwargs["coding_agent_config_defaults"] == { "sonnet": "system.ai.claude-sonnet-4-6" } + def test_managed_parent_schema_pins_configured_claude_default(self, monkeypatch): + managed = { + "enabled_agents": { + "claude": { + "model_config": { + "unity_catalog_location": "system.ai", + "default_model": "system.ai.claude-sonnet-5", + } + } + } + } + + result, _calls, cfg = self._launch(monkeypatch, managed=managed) + + assert result.exit_code == 0, result.output + assert cfg.call_args.kwargs["parent_schema"] == "system.ai" + assert cfg.call_args.kwargs["route_root_model"] == "system.ai.claude-sonnet-5" + assert cfg.call_args.kwargs["coding_agent_config_defaults"] == {} + + def test_managed_parent_schema_honors_recommended_claude_model(self, monkeypatch): + managed = { + "enabled_agents": { + "claude": { + "model_config": { + "unity_catalog_location": "system.ai", + "default_model": "system.ai.claude-sonnet-5", + } + } + } + } + + result, _calls, cfg = self._launch( + monkeypatch, + managed=managed, + recommendation={"agent": "claude", "model": "system.ai.claude-haiku-4-5"}, + ) + + assert result.exit_code == 0, result.output + assert cfg.call_args.kwargs["route_root_model"] == "system.ai.claude-haiku-4-5" + def test_another_agent_keeps_its_own_model_and_is_told_why(self, monkeypatch): # A tier's model belongs to the tier's agent; pinning it on claude would land a Kimi id in # ANTHROPIC_MODEL, which the Anthropic-dialect endpoint cannot serve. From 0e1c417ac5cd85db429b8e3e06aef13e6ba2dbe1 Mon Sep 17 00:00:00 2001 From: Andy Xu Date: Fri, 18 Sep 2026 21:59:34 +0000 Subject: [PATCH 2/5] Propagate managed Claude defaults for MPS --- src/ucode/agents/claude.py | 11 +++++++ src/ucode/cli.py | 17 +++++++--- src/ucode/databricks.py | 2 +- src/ucode/managed_resolve.py | 2 +- tests/test_agent_claude.py | 58 +++++++++++++++++++++++++++++++++-- tests/test_cli.py | 56 ++++++++++++++++++++++++++++++--- tests/test_databricks.py | 5 +++ tests/test_managed_resolve.py | 25 +++++++++++++++ 8 files changed, 164 insertions(+), 12 deletions(-) diff --git a/src/ucode/agents/claude.py b/src/ucode/agents/claude.py index 088f6e17..91df63cb 100644 --- a/src/ucode/agents/claude.py +++ b/src/ucode/agents/claude.py @@ -919,6 +919,17 @@ def write_tool_config( otel_tracing=bool(state.get("claude_otel_tracing")), picker_catalog=picker_catalog, ) + if provider and coding_agent_config_defaults: + # Managed MPS defaults are provider target ids, so write them verbatim into the common + # overlay (including relayed launches, which intentionally skip the OS-managed file). + # The authored family map is complete policy: omitted families must not inherit targets + # derived from the live provider or local discovery. + overlay_env = overlay["env"] + for key in CLAUDE_DEFAULT_MODEL_ENV_KEYS.values(): + overlay_env.pop(key, None) + for family, model_id in coding_agent_config_defaults.items(): + if key := CLAUDE_DEFAULT_MODEL_ENV_KEYS.get(family): + overlay_env[key] = model_id # Native discovery must not inherit UG's prior static allow-list. Keep a replacement picker # written by this launch, and remove only previously owned picker keys that no longer apply. stale_picker_keys = [ diff --git a/src/ucode/cli.py b/src/ucode/cli.py index eab37538..dd59c6f4 100644 --- a/src/ucode/cli.py +++ b/src/ucode/cli.py @@ -2719,17 +2719,26 @@ def _launch_tool( # Native discovery supplies the catalog, but the managed policy still controls # which model Claude starts on. route_root_model = managed_launch_model(managed or {}, recommendation, tool) - if provider and tool == "claude" and (model or provider_models): + provider_launch_model = model + if tool == "claude" and managed_provider: + # A CLI model still wins, followed by the budget recommendation and the managed + # default. Unmanaged providers retain their existing target-selection behavior. + provider_launch_model = provider_launch_model or managed_launch_model( + managed or {}, recommendation, tool + ) + if provider and tool == "claude" and (provider_launch_model or provider_models): if relayed: # Resolve against a curated allowlist so the forwarded id is one the gateway # allows; an allow_all relay declares none, so forward as-is. relayed_forward_model = ( - resolve_provider_launch_model(model, provider_models) + resolve_provider_launch_model(provider_launch_model, provider_models) if provider_models - else model + else provider_launch_model ) else: - route_root_model = resolve_provider_launch_model(model, provider_models or {}) + route_root_model = resolve_provider_launch_model( + provider_launch_model, provider_models or {} + ) if provider and tool == "gemini": # Gemini is the exception: the request still names a concrete model # in the URL, so pin one of the service's targets (--model or default). diff --git a/src/ucode/databricks.py b/src/ucode/databricks.py index 40b19148..2a3b403c 100644 --- a/src/ucode/databricks.py +++ b/src/ucode/databricks.py @@ -2372,7 +2372,7 @@ def map_claude_family_models(targets: list[str]) -> dict[str, str]: # A bare launch pins the first tier the service offers, so it never dead-ends on a model the # gateway 403s. Sonnet first: it's Claude Code's own default tier, so we keep that balanced default # rather than jumping to opus, then fall back to the next offered tier when sonnet isn't allowed. -_CLAUDE_LAUNCH_TIER_PREFERENCE = ("sonnet", "opus", "haiku") +_CLAUDE_LAUNCH_TIER_PREFERENCE = ("sonnet", "opus", "haiku", "fable") def resolve_provider_launch_model(model: str | None, provider_models: dict[str, str]) -> str | None: diff --git a/src/ucode/managed_resolve.py b/src/ucode/managed_resolve.py index 82d1427d..78dd6001 100644 --- a/src/ucode/managed_resolve.py +++ b/src/ucode/managed_resolve.py @@ -249,7 +249,7 @@ def managed_provider_family_models(managed: dict) -> dict[str, str] | None: config = _agent_model_config(managed, "claude") slots: dict[str, str] = {} - raw_slots = _as_dict(config.get("models")) + raw_slots = _as_dict(config.get("default_models_by_model_family")) for slot, family in _CLAUDE_FAMILY_SLOTS.items(): model = _str(raw_slots.get(slot)) if model: diff --git a/tests/test_agent_claude.py b/tests/test_agent_claude.py index d80ed9a8..d07020ee 100644 --- a/tests/test_agent_claude.py +++ b/tests/test_agent_claude.py @@ -1060,6 +1060,40 @@ def test_managed_file_omits_workspace_defaults_for_provider(self, monkeypatch): env = json.loads(managed_writes[0][1])["env"] assert not set(claude.CLAUDE_DEFAULT_MODEL_ENV_KEYS.values()) & env.keys() + def test_managed_file_applies_only_exact_configured_defaults_for_provider(self, monkeypatch): + private_writes: list = [] + managed_writes: list = [] + existing = { + str(FAKE_MANAGED_PATH): { + "env": {"ANTHROPIC_DEFAULT_HAIKU_MODEL": "local.claude-haiku-4-5"} + } + } + self._patch(monkeypatch, private_writes, managed_writes, existing) + state = { + "workspace": WS, + "claude_models": { + "opus": "local.claude-opus-4-8", + "haiku": "local.claude-haiku-4-5", + }, + } + + claude.write_tool_config( + state, + None, + provider="main.default.anthropic-mps", + route_root_model="anthropic.claude-opus-5", + coding_agent_config_defaults={"opus": "anthropic.claude-opus-5"}, + ) + + env = json.loads(managed_writes[0][1])["env"] + assert env["ANTHROPIC_MODEL"] == "anthropic.claude-opus-5" + assert env["ANTHROPIC_DEFAULT_OPUS_MODEL"] == "anthropic.claude-opus-5" + assert "ANTHROPIC_DEFAULT_SONNET_MODEL" not in env + assert "ANTHROPIC_DEFAULT_HAIKU_MODEL" not in env + private_env = private_writes[0][1]["env"] + assert private_env["ANTHROPIC_DEFAULT_OPUS_MODEL"] == "anthropic.claude-opus-5" + assert "ANTHROPIC_DEFAULT_HAIKU_MODEL" not in private_env + def test_managed_file_omits_workspace_defaults_for_parent_schema(self, monkeypatch): private_writes: list = [] managed_writes: list = [] @@ -1413,10 +1447,30 @@ def test_relayed_skips_managed_write(self, monkeypatch): monkeypatch.setattr(claude, "print_warning", lambda msg: warns.append(msg)) monkeypatch.setattr(claude, "relayed_proxy_base_url", lambda state: "http://127.0.0.1:9999") monkeypatch.setattr(claude, "_managed_relayed_conflicts", lambda path: []) - state = {"workspace": WS, "codex_models": []} - claude.write_tool_config(state, "databricks-claude-sonnet-4", relayed=True) + state = { + "workspace": WS, + "claude_models": {"haiku": "local.claude-haiku-4-5"}, + } + defaults = { + "fable": "anthropic.claude-fable-5-1", + "opus": "anthropic.claude-opus-5", + "sonnet": "anthropic.claude-sonnet-5", + "haiku": "anthropic.claude-haiku-4-5", + } + claude.write_tool_config( + state, + "databricks-claude-sonnet-4", + provider="main.default.anthropic-mps", + provider_models={"haiku": "local.claude-haiku-4-5"}, + relayed=True, + coding_agent_config_defaults=defaults, + ) assert managed_writes == [] assert warns == [] + env = private_writes[0][1]["env"] + assert { + family: env[key] for family, key in claude.CLAUDE_DEFAULT_MODEL_ENV_KEYS.items() + } == defaults def test_relayed_fails_on_conflicting_managed_auth(self, monkeypatch): private_writes: list = [] diff --git a/tests/test_cli.py b/tests/test_cli.py index 0d695033..0c19040b 100644 --- a/tests/test_cli.py +++ b/tests/test_cli.py @@ -1013,7 +1013,7 @@ class TestManagedClaudeModelDiscovery: "claude": { "model_config": { "model_provider_service": "main.default.anthropic-mps", - "models": { + "default_models_by_model_family": { "default_sonnet_model": "anthropic.claude-sonnet-4-6", "default_opus_model": "anthropic.claude-opus-4-8", "default_haiku_model": "anthropic.claude-haiku-4-5", @@ -1029,7 +1029,7 @@ class TestManagedClaudeModelDiscovery: } @staticmethod - def _invoke(monkeypatch, managed): + def _invoke(monkeypatch, managed, *, args=None, recommendation=None, relayed=False): state = { **MINIMAL_STATE, "claude_models": {}, @@ -1042,10 +1042,13 @@ def _invoke(monkeypatch, managed): monkeypatch.setattr(cli_mod, "load_state", lambda: state) monkeypatch.setattr(cli_mod, "ensure_provider_state", lambda *_a: state) monkeypatch.setattr(cli_mod, "_fetch_managed_config", lambda _state: (managed, False)) + monkeypatch.setattr( + cli_mod, "_fetch_budget_recommendation", lambda _state, _managed: recommendation + ) monkeypatch.setattr(cli_mod, "get_databricks_token", lambda *_a: "token") monkeypatch.setattr(cli_mod, "get_provider_service", lambda *_a: "main.developer.provider") monkeypatch.setattr(cli_mod, "configure_shared_state", shared) - resolve_provider = MagicMock(return_value=(None, None, False)) + resolve_provider = MagicMock(return_value=(None, None, relayed)) monkeypatch.setattr(cli_mod, "resolve_provider_models", resolve_provider) picker_catalog = db_mod.AnthropicModelCatalog( model_ids=["main.default.claude-sonnet-5"], @@ -1061,7 +1064,7 @@ def _invoke(monkeypatch, managed): monkeypatch.setattr(cli_mod, "configure_tool", configure) monkeypatch.setattr(cli_mod, "launch_agent", launch) - result = runner.invoke(app, ["claude"]) + result = runner.invoke(app, ["claude", *(args or [])]) return { "result": result, "state": state, @@ -1113,8 +1116,53 @@ def test_launch_uses_managed_source_and_native_discovery( calls["list_anthropic_model_catalog"].assert_not_called() assert calls["configure"].call_args.kwargs["picker_catalog"] is None assert "_claude_launch_picker_models" not in calls["launch"].call_args.args[1] + assert calls["configure"].call_args.kwargs["route_root_model"] == ( + "anthropic.claude-sonnet-4-6" + ) + assert calls["configure"].call_args.kwargs["coding_agent_config_defaults"] == { + "fable": "anthropic.claude-fable-5-1", + "opus": "anthropic.claude-opus-4-8", + "sonnet": "anthropic.claude-sonnet-4-6", + "haiku": "anthropic.claude-haiku-4-5", + } assert os.environ["ENABLE_CLAUDE_CODE_GATEWAY_MODEL_DISCOVERY"] == "1" + @pytest.mark.parametrize( + ("args", "recommendation", "relayed", "route_root", "forwarded"), + [ + ( + [], + {"agent": "claude", "model": "anthropic.claude-haiku-4-5"}, + False, + "anthropic.claude-haiku-4-5", + [], + ), + ( + ["--model", "anthropic.claude-opus-4-8"], + {"agent": "claude", "model": "anthropic.claude-haiku-4-5"}, + False, + "anthropic.claude-opus-4-8", + [], + ), + ([], None, True, None, ["--model", "anthropic.claude-sonnet-4-6"]), + ], + ids=["recommendation", "explicit-model", "relayed-default"], + ) + def test_managed_mps_initial_model_precedence( + self, monkeypatch, args, recommendation, relayed, route_root, forwarded + ): + calls = self._invoke( + monkeypatch, + self.MPS_CONFIG, + args=args, + recommendation=recommendation, + relayed=relayed, + ) + + assert calls["result"].exit_code == 0, calls["result"].output + assert calls["configure"].call_args.kwargs["route_root_model"] == route_root + assert calls["launch"].call_args.args[2] == forwarded + def test_claude_discovery_changes_do_not_break_other_managed_providers(): managed = { diff --git a/tests/test_databricks.py b/tests/test_databricks.py index ba6c364f..404dde69 100644 --- a/tests/test_databricks.py +++ b/tests/test_databricks.py @@ -897,6 +897,11 @@ def test_falls_back_to_haiku_when_only_haiku(self): "claude-haiku-4-5" ) + def test_falls_back_to_fable_when_only_fable(self): + assert db_mod.resolve_provider_launch_model(None, {"fable": "claude-fable-5-1"}) == ( + "claude-fable-5-1" + ) + def test_family_alias_resolves_to_declared_target(self): models = {"sonnet": "claude-sonnet-5", "haiku": "claude-haiku-4-5"} assert db_mod.resolve_provider_launch_model("haiku", models) == "claude-haiku-4-5" diff --git a/tests/test_managed_resolve.py b/tests/test_managed_resolve.py index b82a026d..ff0a2c7b 100644 --- a/tests/test_managed_resolve.py +++ b/tests/test_managed_resolve.py @@ -16,6 +16,7 @@ managed_enabled_tools, managed_launch_model, managed_otel_tracing_enabled, + managed_provider_family_models, managed_provider_service, managed_state_overrides, managed_static_models, @@ -169,6 +170,30 @@ def test_family_defaults_propagate_through_the_real_normalize_path(self): "haiku": "system.ai.claude-haiku-4-5", } + def test_provider_family_models_read_the_normalized_slots(self): + managed = normalize_managed_config( + { + "spec_version": 1, + "enabled_agents": [ + { + "agent": "CODING_AGENT_CLAUDE_CODE", + "config": { + "models": {"model_provider_service": "main.default.anthropic-mps"}, + "default_models": { + "default_opus_model": "anthropic.claude-opus-4-8", + "default_sonnet_model": "anthropic.claude-sonnet-4-6", + }, + }, + } + ], + } + ) + + assert managed_provider_family_models(managed) == { + "opus": "anthropic.claude-opus-4-8", + "sonnet": "anthropic.claude-sonnet-4-6", + } + class TestListModels: def test_manifest_list_replaces_local(self): From 09361ea668b2a76b68a1bb83f5d5fbf921fd4ad4 Mon Sep 17 00:00:00 2001 From: Andy Xu Date: Fri, 18 Sep 2026 22:07:08 +0000 Subject: [PATCH 3/5] Unify managed Claude default handling --- src/ucode/agents/claude.py | 41 +++++++++++++++++--------------------- src/ucode/cli.py | 11 ++++++---- tests/test_agent_claude.py | 13 ++++++++++++ 3 files changed, 38 insertions(+), 27 deletions(-) diff --git a/src/ucode/agents/claude.py b/src/ucode/agents/claude.py index 91df63cb..f991fb7b 100644 --- a/src/ucode/agents/claude.py +++ b/src/ucode/agents/claude.py @@ -919,17 +919,20 @@ def write_tool_config( otel_tracing=bool(state.get("claude_otel_tracing")), picker_catalog=picker_catalog, ) - if provider and coding_agent_config_defaults: - # Managed MPS defaults are provider target ids, so write them verbatim into the common - # overlay (including relayed launches, which intentionally skip the OS-managed file). - # The authored family map is complete policy: omitted families must not inherit targets - # derived from the live provider or local discovery. + if (provider or parent_schema) and coding_agent_config_defaults: + # Source-scoped managed defaults belong in the common overlay so they reach both settings + # files (or only the private file for a relay). The authored map is complete policy: + # omitted families must not inherit targets from local or live discovery. overlay_env = overlay["env"] for key in CLAUDE_DEFAULT_MODEL_ENV_KEYS.values(): overlay_env.pop(key, None) for family, model_id in coding_agent_config_defaults.items(): if key := CLAUDE_DEFAULT_MODEL_ENV_KEYS.get(family): - overlay_env[key] = model_id + overlay_env[key] = ( + _maybe_add_1m_suffix(model_id) + if parent_schema and family in ("opus", "sonnet") + else model_id + ) # Native discovery must not inherit UG's prior static allow-list. Keep a replacement picker # written by this launch, and remove only previously owned picker keys that no longer apply. stale_picker_keys = [ @@ -991,21 +994,14 @@ def _compose( if isinstance(last_applied_env.get(key), str) } for family, key in CLAUDE_DEFAULT_MODEL_ENV_KEYS.items(): - if parent_schema is not None: - # A parent schema gets only admin-authored family mappings, never fallback - # defaults from local state or discovery. - selected_default_model = configured_defaults.get(family) - if selected_default_model and family in ("opus", "sonnet"): - selected_default_model = _maybe_add_1m_suffix(selected_default_model) - else: - selected_default_model = _enforce_model_default_hierarchy( - family, - coding_agent_config_defaults=configured_defaults, - settings_file_existing_defaults=settings_file_existing_defaults, - ucode_defaults=ucode_defaults, - ucode_last_written_defaults=ucode_last_written_defaults, - enforced_models=enforced_models, - ) + selected_default_model = _enforce_model_default_hierarchy( + family, + coding_agent_config_defaults=configured_defaults, + settings_file_existing_defaults=settings_file_existing_defaults, + ucode_defaults=ucode_defaults, + ucode_last_written_defaults=ucode_last_written_defaults, + enforced_models=enforced_models, + ) if selected_default_model is None: target_env.pop(key, None) else: @@ -1073,8 +1069,7 @@ def _compose( state, lambda base: _compose( base, - enforce_model_default_hierarchy=provider is None - and (parent_schema is None or bool(coding_agent_config_defaults)), + enforce_model_default_hierarchy=provider is None and parent_schema is None, managed_settings_snapshots=managed_snapshots, ), managed_file_keys, diff --git a/src/ucode/cli.py b/src/ucode/cli.py index dd59c6f4..40479075 100644 --- a/src/ucode/cli.py +++ b/src/ucode/cli.py @@ -2715,17 +2715,20 @@ def _launch_tool( # managed UC discovery likewise lets the agent select from the parent schema. Skip model # resolution, which would otherwise fail when global discovery found no models. resolved_model = None + managed_source_model = ( + managed_launch_model(managed or {}, recommendation, tool) + if tool == "claude" and (managed_provider or managed_parent_schema) + else None + ) if tool == "claude" and managed_parent_schema: # Native discovery supplies the catalog, but the managed policy still controls # which model Claude starts on. - route_root_model = managed_launch_model(managed or {}, recommendation, tool) + route_root_model = managed_source_model provider_launch_model = model if tool == "claude" and managed_provider: # A CLI model still wins, followed by the budget recommendation and the managed # default. Unmanaged providers retain their existing target-selection behavior. - provider_launch_model = provider_launch_model or managed_launch_model( - managed or {}, recommendation, tool - ) + provider_launch_model = provider_launch_model or managed_source_model if provider and tool == "claude" and (provider_launch_model or provider_models): if relayed: # Resolve against a curated allowlist so the forwarded id is one the gateway diff --git a/tests/test_agent_claude.py b/tests/test_agent_claude.py index d07020ee..fecc766c 100644 --- a/tests/test_agent_claude.py +++ b/tests/test_agent_claude.py @@ -1141,6 +1141,10 @@ def test_managed_file_applies_only_configured_defaults_for_parent_schema(self, m assert env["ANTHROPIC_DEFAULT_OPUS_MODEL"] == "system.ai.claude-opus-5[1m]" assert "ANTHROPIC_DEFAULT_SONNET_MODEL" not in env assert "ANTHROPIC_DEFAULT_HAIKU_MODEL" not in env + private_env = private_writes[0][1]["env"] + assert private_env["ANTHROPIC_DEFAULT_OPUS_MODEL"] == "system.ai.claude-opus-5[1m]" + assert "ANTHROPIC_DEFAULT_SONNET_MODEL" not in private_env + assert "ANTHROPIC_DEFAULT_HAIKU_MODEL" not in private_env def test_managed_file_applies_all_configured_defaults_for_parent_schema(self, monkeypatch): private_writes: list = [] @@ -1171,6 +1175,15 @@ def test_managed_file_applies_all_configured_defaults_for_parent_schema(self, mo "sonnet": "system.ai.claude-sonnet-5[1m]", "haiku": "system.ai.claude-haiku-4-5", } + private_env = private_writes[0][1]["env"] + assert { + family: private_env[key] for family, key in claude.CLAUDE_DEFAULT_MODEL_ENV_KEYS.items() + } == { + "fable": "system.ai.claude-fable-5", + "opus": "system.ai.claude-opus-5[1m]", + "sonnet": "system.ai.claude-sonnet-5[1m]", + "haiku": "system.ai.claude-haiku-4-5", + } @pytest.mark.parametrize("with_catalog", [False, True]) def test_parent_schema_prunes_previous_static_picker(self, monkeypatch, with_catalog): From 84d8056bc73114964ecf23cf7e5a606b533e9bdb Mon Sep 17 00:00:00 2001 From: Andy Xu Date: Fri, 18 Sep 2026 22:43:17 +0000 Subject: [PATCH 4/5] Replace managed Claude unit tests with integration coverage --- src/ucode/databricks.py | 2 +- tests/README.md | 5 +- tests/integration/README.md | 7 +- .../test_ug_configure_managed_models.py | 110 ++++++++++++++- tests/test_agent_claude.py | 130 +----------------- tests/test_cli.py | 104 +------------- tests/test_databricks.py | 5 - tests/test_managed_resolve.py | 25 ---- 8 files changed, 125 insertions(+), 263 deletions(-) diff --git a/src/ucode/databricks.py b/src/ucode/databricks.py index 2a3b403c..40b19148 100644 --- a/src/ucode/databricks.py +++ b/src/ucode/databricks.py @@ -2372,7 +2372,7 @@ def map_claude_family_models(targets: list[str]) -> dict[str, str]: # A bare launch pins the first tier the service offers, so it never dead-ends on a model the # gateway 403s. Sonnet first: it's Claude Code's own default tier, so we keep that balanced default # rather than jumping to opus, then fall back to the next offered tier when sonnet isn't allowed. -_CLAUDE_LAUNCH_TIER_PREFERENCE = ("sonnet", "opus", "haiku", "fable") +_CLAUDE_LAUNCH_TIER_PREFERENCE = ("sonnet", "opus", "haiku") def resolve_provider_launch_model(model: str | None, provider_models: dict[str, str]) -> str | None: diff --git a/tests/README.md b/tests/README.md index 0490b7e0..4458a0dd 100644 --- a/tests/README.md +++ b/tests/README.md @@ -58,6 +58,7 @@ All tests live directly in `integration/`; shared mechanics live in `utils/`. | `test_case_06_*`, `test_case_08_*` | Pass a provider or model-location override to managed Codex after configure and from fresh state | ug rejects the override before Codex starts and preserves agent-owned state | | `test_case_10_*`, `test_case_12_*` | Disable discovery and pass a provider or model-location override to managed Codex | ug still rejects both configured and fresh launches | | `test_ug_configure_managed_codex_catalog_fallback` | Configure from an injected managed response containing a GPT model absent from Codex's bundled catalog | Actionable metadata warning; conservative catalog entry for the unknown model; real Codex prompt on the valid default model | +| `test_managed_fixture_claude_mps_defaults_accompany_discovery`, `test_managed_fixture_claude_parent_schema_defaults_accompany_discovery` | Launch Claude from injected managed defaults with MPS and Unity Catalog discovery | Both generated settings files retain every admin-authored default alongside the source header; only UC Opus/Sonnet family ids gain `[1m]` | | `test_managed_fixture_claude_model_lifecycle`, `test_managed_fixture_codex_model_lifecycle` | Configure across no config -> static A -> static B -> MPS -> no config (stub-injected, `null` for no-config; MPS via a real provider service) | Each agent's model files reconcile to each static config (removed models pruned); switching to an MPS and a workspace with no managed config both clear ug's managed model settings so no stale list is enforced | | `test_ug_installed_wheel_exposes_help_and_version` | Invoke freshly installed console command | Package version matches; public help works | | `test_ug_status_in_fresh_home_is_unconfigured` | Request status before configure | Unconfigured status | @@ -67,9 +68,9 @@ All tests live directly in `integration/`; shared mechanics live in `utils/`. With both agents selected there are **41 live cases** (6 interactive TUI cases), **4 managed-workspace cases** (marker `managed`, run against a separate workspace that -publishes a CodingAgentConfig), **34 managed-fixture cases** (marker `managed_fixture`, with only +publishes a CodingAgentConfig), **36 managed-fixture cases** (marker `managed_fixture`, with only the CodingAgentConfig input injected), and **5 installation checks**. The 24 numbered scenarios -cover configured and fresh state across the Claude and Codex managed-discovery matrix; ten +cover configured and fresh state across the Claude and Codex managed-discovery matrix; twelve existing collected cases cover focused model, MCP, skills, and lifecycle shapes. Parametrization varies argument spelling or routing mode, never hides the agent/provider in the test name. Duplicate boot-only cases diff --git a/tests/integration/README.md b/tests/integration/README.md index 224cfb34..26a92f0c 100644 --- a/tests/integration/README.md +++ b/tests/integration/README.md @@ -162,12 +162,13 @@ fails the selected CUJ, rather than skipping it. There are **42 live cases** (including 6 TUI journeys) and **5 installation checks** with both agents. A separate **4 managed-workspace cases** (one per agent, an idempotent re-configure, and a cache-TTL journey; marker `managed`) run against a workspace that publishes a -CodingAgentConfig; see "Managed-workspace journeys" below. A further **34 `managed_fixture` +CodingAgentConfig; see "Managed-workspace journeys" below. A further **36 `managed_fixture` cases** use `UCODE_MANAGED_CONFIG_STUB`. Twenty-four explicit configured/fresh Claude and Codex discovery and source-override journeys fetch the published config once per agent, replace that -agent's static source with its dedicated MPS, and reuse the result. Ten existing collected cases +agent's static source with its dedicated MPS, and reuse the result. Twelve existing collected cases cover focused model, MCP, skills, and lifecycle shapes, including per-agent model reconciliation -and managed skill cleanup. +and managed skill cleanup. The two Claude default-model cases launch with injected MPS and Unity +Catalog sources and verify both generated settings files retain all admin-authored family defaults. See the named coverage and gaps matrix in [../README.md](../README.md). diff --git a/tests/integration/test_ug_configure_managed_models.py b/tests/integration/test_ug_configure_managed_models.py index 296edc65..6a6ace23 100644 --- a/tests/integration/test_ug_configure_managed_models.py +++ b/tests/integration/test_ug_configure_managed_models.py @@ -8,6 +8,7 @@ import json import pytest +from utils.constants import MANAGED_CLAUDE_PROVIDER_SERVICE from utils.evidence import FileTask from utils.managed import ( build_claude_agent_config, @@ -15,7 +16,7 @@ build_coding_agent_config, set_managed_config_stub, ) -from utils.terminal import AgentTerminal +from utils.terminal import AgentTerminal, TerminalProcess CLAUDE_OPUS = "system.ai.claude-opus-4-8" # A real ca-central model absent from the live published config: its presence in the picker can @@ -24,6 +25,12 @@ LIVE_ONLY = "haiku-4-5" # published live, but not in the injected list below CODEX_DEFAULT = "system.ai.gpt-5-6-sol" CODEX_WITHOUT_BUNDLED_METADATA = "system.ai.gpt-99" +MANAGED_CLAUDE_DEFAULT_ENV_KEYS = { + "default_fable_model": "ANTHROPIC_DEFAULT_FABLE_MODEL", + "default_opus_model": "ANTHROPIC_DEFAULT_OPUS_MODEL", + "default_sonnet_model": "ANTHROPIC_DEFAULT_SONNET_MODEL", + "default_haiku_model": "ANTHROPIC_DEFAULT_HAIKU_MODEL", +} SMART_ROUTING_BANNER = "Using Unity Gateway Smart Router." CLAUDE_SMART_ROUTING_MODELS = [ @@ -44,6 +51,107 @@ ] +@pytest.mark.managed_fixture +@pytest.mark.claude +def test_managed_fixture_claude_mps_defaults_accompany_discovery(live_session, workspace, tmp_path): + """Scenario: launch Claude with managed defaults and MPS discovery. + + Expected: the installed ug launch writes the MPS header and every admin-authored default to + both Claude settings files without changing the model ids. This settings reconciliation check + does not claim model inference. + """ + session = live_session + defaults = { + "default_model": "anthropic.claude-sonnet-5", + "default_fable_model": "anthropic.claude-fable-5-1", + "default_opus_model": "anthropic.claude-opus-5", + "default_sonnet_model": "anthropic.claude-sonnet-5", + "default_haiku_model": "anthropic.claude-haiku-4-5", + } + config = build_coding_agent_config( + "CODING_AGENT_CLAUDE_CODE", + { + "agent": "CODING_AGENT_CLAUDE_CODE", + "config": { + "models": {"model_provider_service": MANAGED_CLAUDE_PROVIDER_SERVICE}, + "default_models": defaults, + }, + }, + ) + set_managed_config_stub(session, tmp_path, config) + result = session.run("configure", "--workspace", workspace, "--skip-upgrade", timeout=240) + assert "Select coding agents to configure:" not in result.stdout, result.stdout + + command = [str(session.binary), "claude", "--", "--version"] + with TerminalProcess(session, "claude", command, "managed-defaults-mps") as terminal: + terminal.finish(timeout=240) + + private_settings = json.loads((session.home / ".claude" / "ucode-settings.json").read_text()) + os_managed_settings = json.loads( + session.run("/etc/claude-code/managed-settings.json", binary="cat", timeout=30).stdout + ) + for settings in (private_settings, os_managed_settings): + env = settings.get("env") or {} + expected_header = f"Databricks-Model-Provider-Service: {MANAGED_CLAUDE_PROVIDER_SERVICE}" + assert expected_header in env.get("ANTHROPIC_CUSTOM_HEADERS", "").splitlines(), settings + assert env.get("ANTHROPIC_MODEL") == defaults["default_model"], settings + for config_key, env_key in MANAGED_CLAUDE_DEFAULT_ENV_KEYS.items(): + assert env.get(env_key) == defaults[config_key], settings + + +@pytest.mark.managed_fixture +@pytest.mark.claude +def test_managed_fixture_claude_parent_schema_defaults_accompany_discovery( + live_session, workspace, parent_schema, tmp_path +): + """Scenario: launch Claude with managed defaults and Unity Catalog discovery. + + Expected: the installed ug launch writes the parent-schema header and every admin-authored + default to both Claude settings files, adding ``[1m]`` only to Opus and Sonnet family defaults. + This settings reconciliation check does not claim model inference. + """ + session = live_session + defaults = { + "default_model": f"{parent_schema}.claude-sonnet-5", + "default_fable_model": f"{parent_schema}.claude-fable-5-1", + "default_opus_model": f"{parent_schema}.claude-opus-5", + "default_sonnet_model": f"{parent_schema}.claude-sonnet-5", + "default_haiku_model": f"{parent_schema}.claude-haiku-4-5", + } + config = build_coding_agent_config( + "CODING_AGENT_CLAUDE_CODE", + { + "agent": "CODING_AGENT_CLAUDE_CODE", + "config": { + "models": {"unity_catalog_location": parent_schema}, + "default_models": defaults, + }, + }, + ) + set_managed_config_stub(session, tmp_path, config) + result = session.run("configure", "--workspace", workspace, "--skip-upgrade", timeout=240) + assert "Select coding agents to configure:" not in result.stdout, result.stdout + + command = [str(session.binary), "claude", "--", "--version"] + with TerminalProcess(session, "claude", command, "managed-defaults-parent-schema") as terminal: + terminal.finish(timeout=240) + + private_settings = json.loads((session.home / ".claude" / "ucode-settings.json").read_text()) + os_managed_settings = json.loads( + session.run("/etc/claude-code/managed-settings.json", binary="cat", timeout=30).stdout + ) + for settings in (private_settings, os_managed_settings): + env = settings.get("env") or {} + expected_header = f"Databricks-Model-Service-Parent-Schema: {parent_schema}" + assert expected_header in env.get("ANTHROPIC_CUSTOM_HEADERS", "").splitlines(), settings + assert env.get("ANTHROPIC_MODEL") == defaults["default_model"], settings + for config_key, env_key in MANAGED_CLAUDE_DEFAULT_ENV_KEYS.items(): + expected = defaults[config_key] + if config_key in {"default_opus_model", "default_sonnet_model"}: + expected += "[1m]" + assert env.get(env_key) == expected, settings + + @pytest.mark.managed_fixture @pytest.mark.claude def test_managed_fixture_claude_model_picker_reflects_the_config(live_session, workspace, tmp_path): diff --git a/tests/test_agent_claude.py b/tests/test_agent_claude.py index fecc766c..c08f76a2 100644 --- a/tests/test_agent_claude.py +++ b/tests/test_agent_claude.py @@ -1060,40 +1060,6 @@ def test_managed_file_omits_workspace_defaults_for_provider(self, monkeypatch): env = json.loads(managed_writes[0][1])["env"] assert not set(claude.CLAUDE_DEFAULT_MODEL_ENV_KEYS.values()) & env.keys() - def test_managed_file_applies_only_exact_configured_defaults_for_provider(self, monkeypatch): - private_writes: list = [] - managed_writes: list = [] - existing = { - str(FAKE_MANAGED_PATH): { - "env": {"ANTHROPIC_DEFAULT_HAIKU_MODEL": "local.claude-haiku-4-5"} - } - } - self._patch(monkeypatch, private_writes, managed_writes, existing) - state = { - "workspace": WS, - "claude_models": { - "opus": "local.claude-opus-4-8", - "haiku": "local.claude-haiku-4-5", - }, - } - - claude.write_tool_config( - state, - None, - provider="main.default.anthropic-mps", - route_root_model="anthropic.claude-opus-5", - coding_agent_config_defaults={"opus": "anthropic.claude-opus-5"}, - ) - - env = json.loads(managed_writes[0][1])["env"] - assert env["ANTHROPIC_MODEL"] == "anthropic.claude-opus-5" - assert env["ANTHROPIC_DEFAULT_OPUS_MODEL"] == "anthropic.claude-opus-5" - assert "ANTHROPIC_DEFAULT_SONNET_MODEL" not in env - assert "ANTHROPIC_DEFAULT_HAIKU_MODEL" not in env - private_env = private_writes[0][1]["env"] - assert private_env["ANTHROPIC_DEFAULT_OPUS_MODEL"] == "anthropic.claude-opus-5" - assert "ANTHROPIC_DEFAULT_HAIKU_MODEL" not in private_env - def test_managed_file_omits_workspace_defaults_for_parent_schema(self, monkeypatch): private_writes: list = [] managed_writes: list = [] @@ -1113,78 +1079,6 @@ def test_managed_file_omits_workspace_defaults_for_parent_schema(self, monkeypat env = json.loads(managed_writes[0][1])["env"] assert not set(claude.CLAUDE_DEFAULT_MODEL_ENV_KEYS.values()) & env.keys() - def test_managed_file_applies_only_configured_defaults_for_parent_schema(self, monkeypatch): - private_writes: list = [] - managed_writes: list = [] - existing = { - str(FAKE_MANAGED_PATH): { - "env": {"ANTHROPIC_DEFAULT_HAIKU_MODEL": "system.ai.claude-haiku-4-5"} - } - } - self._patch(monkeypatch, private_writes, managed_writes, existing) - state = { - "workspace": WS, - "claude_models": { - "opus": "developer.claude-opus-4-8", - "sonnet": "developer.claude-sonnet-4-6", - }, - } - - claude.write_tool_config( - state, - None, - coding_agent_config_defaults={"opus": "system.ai.claude-opus-5"}, - parent_schema="system.ai", - ) - - env = json.loads(managed_writes[0][1])["env"] - assert env["ANTHROPIC_DEFAULT_OPUS_MODEL"] == "system.ai.claude-opus-5[1m]" - assert "ANTHROPIC_DEFAULT_SONNET_MODEL" not in env - assert "ANTHROPIC_DEFAULT_HAIKU_MODEL" not in env - private_env = private_writes[0][1]["env"] - assert private_env["ANTHROPIC_DEFAULT_OPUS_MODEL"] == "system.ai.claude-opus-5[1m]" - assert "ANTHROPIC_DEFAULT_SONNET_MODEL" not in private_env - assert "ANTHROPIC_DEFAULT_HAIKU_MODEL" not in private_env - - def test_managed_file_applies_all_configured_defaults_for_parent_schema(self, monkeypatch): - private_writes: list = [] - managed_writes: list = [] - self._patch(monkeypatch, private_writes, managed_writes) - defaults = { - "fable": "system.ai.claude-fable-5", - "opus": "system.ai.claude-opus-5", - "sonnet": "system.ai.claude-sonnet-5", - "haiku": "system.ai.claude-haiku-4-5", - } - - claude.write_tool_config( - {"workspace": WS, "claude_models": defaults}, - None, - route_root_model="system.ai.claude-sonnet-5", - coding_agent_config_defaults=defaults, - parent_schema="system.ai", - ) - - env = json.loads(managed_writes[0][1])["env"] - assert env["ANTHROPIC_MODEL"] == "system.ai.claude-sonnet-5" - assert { - family: env[key] for family, key in claude.CLAUDE_DEFAULT_MODEL_ENV_KEYS.items() - } == { - "fable": "system.ai.claude-fable-5", - "opus": "system.ai.claude-opus-5[1m]", - "sonnet": "system.ai.claude-sonnet-5[1m]", - "haiku": "system.ai.claude-haiku-4-5", - } - private_env = private_writes[0][1]["env"] - assert { - family: private_env[key] for family, key in claude.CLAUDE_DEFAULT_MODEL_ENV_KEYS.items() - } == { - "fable": "system.ai.claude-fable-5", - "opus": "system.ai.claude-opus-5[1m]", - "sonnet": "system.ai.claude-sonnet-5[1m]", - "haiku": "system.ai.claude-haiku-4-5", - } - @pytest.mark.parametrize("with_catalog", [False, True]) def test_parent_schema_prunes_previous_static_picker(self, monkeypatch, with_catalog): private_writes: list = [] @@ -1460,30 +1354,10 @@ def test_relayed_skips_managed_write(self, monkeypatch): monkeypatch.setattr(claude, "print_warning", lambda msg: warns.append(msg)) monkeypatch.setattr(claude, "relayed_proxy_base_url", lambda state: "http://127.0.0.1:9999") monkeypatch.setattr(claude, "_managed_relayed_conflicts", lambda path: []) - state = { - "workspace": WS, - "claude_models": {"haiku": "local.claude-haiku-4-5"}, - } - defaults = { - "fable": "anthropic.claude-fable-5-1", - "opus": "anthropic.claude-opus-5", - "sonnet": "anthropic.claude-sonnet-5", - "haiku": "anthropic.claude-haiku-4-5", - } - claude.write_tool_config( - state, - "databricks-claude-sonnet-4", - provider="main.default.anthropic-mps", - provider_models={"haiku": "local.claude-haiku-4-5"}, - relayed=True, - coding_agent_config_defaults=defaults, - ) + state = {"workspace": WS, "codex_models": []} + claude.write_tool_config(state, "databricks-claude-sonnet-4", relayed=True) assert managed_writes == [] assert warns == [] - env = private_writes[0][1]["env"] - assert { - family: env[key] for family, key in claude.CLAUDE_DEFAULT_MODEL_ENV_KEYS.items() - } == defaults def test_relayed_fails_on_conflicting_managed_auth(self, monkeypatch): private_writes: list = [] diff --git a/tests/test_cli.py b/tests/test_cli.py index 0c19040b..4a415b58 100644 --- a/tests/test_cli.py +++ b/tests/test_cli.py @@ -1013,7 +1013,7 @@ class TestManagedClaudeModelDiscovery: "claude": { "model_config": { "model_provider_service": "main.default.anthropic-mps", - "default_models_by_model_family": { + "models": { "default_sonnet_model": "anthropic.claude-sonnet-4-6", "default_opus_model": "anthropic.claude-opus-4-8", "default_haiku_model": "anthropic.claude-haiku-4-5", @@ -1029,7 +1029,7 @@ class TestManagedClaudeModelDiscovery: } @staticmethod - def _invoke(monkeypatch, managed, *, args=None, recommendation=None, relayed=False): + def _invoke(monkeypatch, managed): state = { **MINIMAL_STATE, "claude_models": {}, @@ -1042,13 +1042,10 @@ def _invoke(monkeypatch, managed, *, args=None, recommendation=None, relayed=Fal monkeypatch.setattr(cli_mod, "load_state", lambda: state) monkeypatch.setattr(cli_mod, "ensure_provider_state", lambda *_a: state) monkeypatch.setattr(cli_mod, "_fetch_managed_config", lambda _state: (managed, False)) - monkeypatch.setattr( - cli_mod, "_fetch_budget_recommendation", lambda _state, _managed: recommendation - ) monkeypatch.setattr(cli_mod, "get_databricks_token", lambda *_a: "token") monkeypatch.setattr(cli_mod, "get_provider_service", lambda *_a: "main.developer.provider") monkeypatch.setattr(cli_mod, "configure_shared_state", shared) - resolve_provider = MagicMock(return_value=(None, None, relayed)) + resolve_provider = MagicMock(return_value=(None, None, False)) monkeypatch.setattr(cli_mod, "resolve_provider_models", resolve_provider) picker_catalog = db_mod.AnthropicModelCatalog( model_ids=["main.default.claude-sonnet-5"], @@ -1064,7 +1061,7 @@ def _invoke(monkeypatch, managed, *, args=None, recommendation=None, relayed=Fal monkeypatch.setattr(cli_mod, "configure_tool", configure) monkeypatch.setattr(cli_mod, "launch_agent", launch) - result = runner.invoke(app, ["claude", *(args or [])]) + result = runner.invoke(app, ["claude"]) return { "result": result, "state": state, @@ -1116,53 +1113,8 @@ def test_launch_uses_managed_source_and_native_discovery( calls["list_anthropic_model_catalog"].assert_not_called() assert calls["configure"].call_args.kwargs["picker_catalog"] is None assert "_claude_launch_picker_models" not in calls["launch"].call_args.args[1] - assert calls["configure"].call_args.kwargs["route_root_model"] == ( - "anthropic.claude-sonnet-4-6" - ) - assert calls["configure"].call_args.kwargs["coding_agent_config_defaults"] == { - "fable": "anthropic.claude-fable-5-1", - "opus": "anthropic.claude-opus-4-8", - "sonnet": "anthropic.claude-sonnet-4-6", - "haiku": "anthropic.claude-haiku-4-5", - } assert os.environ["ENABLE_CLAUDE_CODE_GATEWAY_MODEL_DISCOVERY"] == "1" - @pytest.mark.parametrize( - ("args", "recommendation", "relayed", "route_root", "forwarded"), - [ - ( - [], - {"agent": "claude", "model": "anthropic.claude-haiku-4-5"}, - False, - "anthropic.claude-haiku-4-5", - [], - ), - ( - ["--model", "anthropic.claude-opus-4-8"], - {"agent": "claude", "model": "anthropic.claude-haiku-4-5"}, - False, - "anthropic.claude-opus-4-8", - [], - ), - ([], None, True, None, ["--model", "anthropic.claude-sonnet-4-6"]), - ], - ids=["recommendation", "explicit-model", "relayed-default"], - ) - def test_managed_mps_initial_model_precedence( - self, monkeypatch, args, recommendation, relayed, route_root, forwarded - ): - calls = self._invoke( - monkeypatch, - self.MPS_CONFIG, - args=args, - recommendation=recommendation, - relayed=relayed, - ) - - assert calls["result"].exit_code == 0, calls["result"].output - assert calls["configure"].call_args.kwargs["route_root_model"] == route_root - assert calls["launch"].call_args.args[2] == forwarded - def test_claude_discovery_changes_do_not_break_other_managed_providers(): managed = { @@ -4853,16 +4805,14 @@ def test_the_recommended_agent_gets_the_recommended_model(self, monkeypatch): ) assert cfg.call_args.args[2] == "system.ai.claude-haiku-4-5" - def test_managed_parent_schema_passes_configured_claude_defaults_to_writer(self, monkeypatch): + def test_passes_configured_claude_defaults_to_writer(self, monkeypatch): managed = { "enabled_agents": { "claude": { "model_config": { - "unity_catalog_location": "system.ai", - "default_model": "system.ai.claude-sonnet-4-6", "default_models_by_model_family": { "default_sonnet_model": "system.ai.claude-sonnet-4-6", - }, + } } } } @@ -4871,52 +4821,10 @@ def test_managed_parent_schema_passes_configured_claude_defaults_to_writer(self, result, _calls, cfg = self._launch(monkeypatch, managed=managed) assert result.exit_code == 0, result.output - assert cfg.call_args.kwargs["parent_schema"] == "system.ai" - assert cfg.call_args.kwargs["route_root_model"] == "system.ai.claude-sonnet-4-6" assert cfg.call_args.kwargs["coding_agent_config_defaults"] == { "sonnet": "system.ai.claude-sonnet-4-6" } - def test_managed_parent_schema_pins_configured_claude_default(self, monkeypatch): - managed = { - "enabled_agents": { - "claude": { - "model_config": { - "unity_catalog_location": "system.ai", - "default_model": "system.ai.claude-sonnet-5", - } - } - } - } - - result, _calls, cfg = self._launch(monkeypatch, managed=managed) - - assert result.exit_code == 0, result.output - assert cfg.call_args.kwargs["parent_schema"] == "system.ai" - assert cfg.call_args.kwargs["route_root_model"] == "system.ai.claude-sonnet-5" - assert cfg.call_args.kwargs["coding_agent_config_defaults"] == {} - - def test_managed_parent_schema_honors_recommended_claude_model(self, monkeypatch): - managed = { - "enabled_agents": { - "claude": { - "model_config": { - "unity_catalog_location": "system.ai", - "default_model": "system.ai.claude-sonnet-5", - } - } - } - } - - result, _calls, cfg = self._launch( - monkeypatch, - managed=managed, - recommendation={"agent": "claude", "model": "system.ai.claude-haiku-4-5"}, - ) - - assert result.exit_code == 0, result.output - assert cfg.call_args.kwargs["route_root_model"] == "system.ai.claude-haiku-4-5" - def test_another_agent_keeps_its_own_model_and_is_told_why(self, monkeypatch): # A tier's model belongs to the tier's agent; pinning it on claude would land a Kimi id in # ANTHROPIC_MODEL, which the Anthropic-dialect endpoint cannot serve. diff --git a/tests/test_databricks.py b/tests/test_databricks.py index 404dde69..ba6c364f 100644 --- a/tests/test_databricks.py +++ b/tests/test_databricks.py @@ -897,11 +897,6 @@ def test_falls_back_to_haiku_when_only_haiku(self): "claude-haiku-4-5" ) - def test_falls_back_to_fable_when_only_fable(self): - assert db_mod.resolve_provider_launch_model(None, {"fable": "claude-fable-5-1"}) == ( - "claude-fable-5-1" - ) - def test_family_alias_resolves_to_declared_target(self): models = {"sonnet": "claude-sonnet-5", "haiku": "claude-haiku-4-5"} assert db_mod.resolve_provider_launch_model("haiku", models) == "claude-haiku-4-5" diff --git a/tests/test_managed_resolve.py b/tests/test_managed_resolve.py index ff0a2c7b..b82a026d 100644 --- a/tests/test_managed_resolve.py +++ b/tests/test_managed_resolve.py @@ -16,7 +16,6 @@ managed_enabled_tools, managed_launch_model, managed_otel_tracing_enabled, - managed_provider_family_models, managed_provider_service, managed_state_overrides, managed_static_models, @@ -170,30 +169,6 @@ def test_family_defaults_propagate_through_the_real_normalize_path(self): "haiku": "system.ai.claude-haiku-4-5", } - def test_provider_family_models_read_the_normalized_slots(self): - managed = normalize_managed_config( - { - "spec_version": 1, - "enabled_agents": [ - { - "agent": "CODING_AGENT_CLAUDE_CODE", - "config": { - "models": {"model_provider_service": "main.default.anthropic-mps"}, - "default_models": { - "default_opus_model": "anthropic.claude-opus-4-8", - "default_sonnet_model": "anthropic.claude-sonnet-4-6", - }, - }, - } - ], - } - ) - - assert managed_provider_family_models(managed) == { - "opus": "anthropic.claude-opus-4-8", - "sonnet": "anthropic.claude-sonnet-4-6", - } - class TestListModels: def test_manifest_list_replaces_local(self): From 21de99fdfcfee9da4a3e7b87d4b9a5821536a6dd Mon Sep 17 00:00:00 2001 From: Andy Xu Date: Fri, 18 Sep 2026 23:22:50 +0000 Subject: [PATCH 5/5] Reuse Claude default hierarchy for managed sources --- src/ucode/agents/claude.py | 49 +++++++++---------- .../test_ug_configure_managed_models.py | 3 +- 2 files changed, 25 insertions(+), 27 deletions(-) diff --git a/src/ucode/agents/claude.py b/src/ucode/agents/claude.py index f991fb7b..8eb4a5b5 100644 --- a/src/ucode/agents/claude.py +++ b/src/ucode/agents/claude.py @@ -582,6 +582,7 @@ def _enforce_model_default_hierarchy( ucode_defaults: dict[str, str], ucode_last_written_defaults: dict[str, str], enforced_models: list[str] | None, + add_1m_suffix: bool = True, ) -> str | None: """Resolve one Claude family's managed-file default model. @@ -598,7 +599,7 @@ def _enforce_model_default_hierarchy( selected = ucode_defaults.get(family) if selected is None: return None - if family in ("opus", "sonnet"): + if add_1m_suffix and family in ("opus", "sonnet"): selected = _maybe_add_1m_suffix(selected) if enforced_models is not None and selected.split("[", 1)[0] not in enforced_models: return None @@ -919,20 +920,7 @@ def write_tool_config( otel_tracing=bool(state.get("claude_otel_tracing")), picker_catalog=picker_catalog, ) - if (provider or parent_schema) and coding_agent_config_defaults: - # Source-scoped managed defaults belong in the common overlay so they reach both settings - # files (or only the private file for a relay). The authored map is complete policy: - # omitted families must not inherit targets from local or live discovery. - overlay_env = overlay["env"] - for key in CLAUDE_DEFAULT_MODEL_ENV_KEYS.values(): - overlay_env.pop(key, None) - for family, model_id in coding_agent_config_defaults.items(): - if key := CLAUDE_DEFAULT_MODEL_ENV_KEYS.get(family): - overlay_env[key] = ( - _maybe_add_1m_suffix(model_id) - if parent_schema and family in ("opus", "sonnet") - else model_id - ) + source_scoped_defaults = bool((provider or parent_schema) and coding_agent_config_defaults) # Native discovery must not inherit UG's prior static allow-list. Keep a replacement picker # written by this launch, and remove only previously owned picker keys that no longer apply. stale_picker_keys = [ @@ -971,15 +959,21 @@ def _compose( settings_file_env = base_env if isinstance(base_env, dict) else {} target_env = overlay_for_merge["env"] configured_defaults = coding_agent_config_defaults or {} - settings_file_existing_defaults = { - family: model - for family, key in CLAUDE_DEFAULT_MODEL_ENV_KEYS.items() - if isinstance((model := settings_file_env.get(key)), str) - } - managed_overlay = state.get(MANAGED_OVERLAY_KEY, {}) - ucode_defaults = ( - managed_overlay.get("claude_models") or state.get("claude_models") or {} - ) + if source_scoped_defaults: + # The managed map is complete policy for this source: omitted families must not + # inherit targets from local settings or live discovery. + settings_file_existing_defaults = {} + ucode_defaults = {} + else: + settings_file_existing_defaults = { + family: model + for family, key in CLAUDE_DEFAULT_MODEL_ENV_KEYS.items() + if isinstance((model := settings_file_env.get(key)), str) + } + managed_overlay = state.get(MANAGED_OVERLAY_KEY, {}) + ucode_defaults = ( + managed_overlay.get("claude_models") or state.get("claude_models") or {} + ) enforced_models = overlay_for_merge.get("availableModels") last_applied_env = {} @@ -1001,6 +995,7 @@ def _compose( ucode_defaults=ucode_defaults, ucode_last_written_defaults=ucode_last_written_defaults, enforced_models=enforced_models, + add_1m_suffix=provider is None, ) if selected_default_model is None: target_env.pop(key, None) @@ -1060,7 +1055,7 @@ def _compose( CLAUDE_SETTINGS_PATH, _compose( read_json_safe(CLAUDE_SETTINGS_PATH), - enforce_model_default_hierarchy=False, + enforce_model_default_hierarchy=source_scoped_defaults, managed_settings_snapshots=None, ), ) @@ -1069,7 +1064,9 @@ def _compose( state, lambda base: _compose( base, - enforce_model_default_hierarchy=provider is None and parent_schema is None, + enforce_model_default_hierarchy=( + source_scoped_defaults or (provider is None and parent_schema is None) + ), managed_settings_snapshots=managed_snapshots, ), managed_file_keys, diff --git a/tests/integration/test_ug_configure_managed_models.py b/tests/integration/test_ug_configure_managed_models.py index 6a6ace23..c87abb93 100644 --- a/tests/integration/test_ug_configure_managed_models.py +++ b/tests/integration/test_ug_configure_managed_models.py @@ -102,7 +102,7 @@ def test_managed_fixture_claude_mps_defaults_accompany_discovery(live_session, w @pytest.mark.managed_fixture @pytest.mark.claude def test_managed_fixture_claude_parent_schema_defaults_accompany_discovery( - live_session, workspace, parent_schema, tmp_path + live_session, workspace, tmp_path ): """Scenario: launch Claude with managed defaults and Unity Catalog discovery. @@ -111,6 +111,7 @@ def test_managed_fixture_claude_parent_schema_defaults_accompany_discovery( This settings reconciliation check does not claim model inference. """ session = live_session + parent_schema = "system.ai" defaults = { "default_model": f"{parent_schema}.claude-sonnet-5", "default_fable_model": f"{parent_schema}.claude-fable-5-1",