From c832bd4794e04603f3687265ce8fdcad7a45c9d3 Mon Sep 17 00:00:00 2001 From: Xi Lyu Date: Fri, 23 Oct 2026 15:26:42 +0000 Subject: [PATCH 1/2] copilot: route ug copilot through Microsoft Foundry provider services Foundry's Claude targets use the Anthropic wire whatever they declare (Foundry serves Claude on Anthropic Messages, not Chat Completions); its GPT 5+ and o-series targets use Responses and the rest Chat Completions, as for OpenAI and Azure OpenAI services. Co-authored-by: Isaac --- README.md | 2 +- src/ucode/agents/copilot.py | 8 ++++++- src/ucode/databricks.py | 16 +++++++++---- src/ucode/managed_setup.py | 6 ++--- tests/integration/README.md | 11 +++++---- .../integration/test_ug_copilot_providers.py | 24 +++++++++++++++++++ tests/test_agent_copilot.py | 17 +++++++++++++ tests/test_databricks.py | 12 ++++++++-- tests/test_managed_setup.py | 12 +++++++--- 9 files changed, 89 insertions(+), 19 deletions(-) diff --git a/README.md b/README.md index 01c122ce9..9658dbc86 100644 --- a/README.md +++ b/README.md @@ -91,7 +91,7 @@ restart `ug copilot`. Proxy errors are logged to `~/.ucode/logs/copilot-proxy.lo `ug copilot --provider ..` routes the session through a Model Provider Service instead, and `/model` lists that service's models: Claude models of an Anthropic or -Bedrock service, and the chat models of an OpenAI or Azure OpenAI service. For a service that +Bedrock service, and the chat models of an OpenAI, Azure OpenAI or Microsoft Foundry service. For a service that allows every model, `/model` lists the models the gateway reports for it, which the provider account may not serve; name the one to start on with `--model`. Services that relay a Claude subscription work only with Claude Code. Interactive `ug configure` offers the matching services under **External Models**, and a managed workspace diff --git a/src/ucode/agents/copilot.py b/src/ucode/agents/copilot.py index dcb1df2ef..3d091088c 100644 --- a/src/ucode/agents/copilot.py +++ b/src/ucode/agents/copilot.py @@ -61,6 +61,7 @@ from ucode.databricks import ( ANTHROPIC_API_PROVIDER_TYPES, ANTHROPIC_FAMILIES, + MICROSOFT_FOUNDRY_PROVIDER_TYPE, ModelServiceEntry, bare_model_name, build_copilot_base_url, @@ -192,7 +193,8 @@ def classify_model( they don't declare. Without ``provider_type``, ``model`` is a Databricks model service and ``api_types`` its advertised routes. With it, ``model`` is a Model Provider Service target and ``api_types`` its declared APIs: Anthropic-API provider types serve only Claude, and other types - put every model that isn't GPT 5+ or o-series on Chat Completions.""" + put every model that isn't GPT 5+ or o-series on Chat Completions, except Foundry's Claude + models, which use Anthropic.""" declared = tuple(api_types) tail = bare_model_name(model) if provider_type is None: @@ -208,6 +210,10 @@ def classify_model( return None, "ug copilot uses only the Claude models of this service" return wire, None if provider_type is not None: + # Foundry serves its Claude targets on the Anthropic route but not on Chat Completions, + # whatever the targets declare. + if wire == WIRE_ANTHROPIC and provider_type == MICROSOFT_FOUNDRY_PROVIDER_TYPE: + return wire, None return (WIRE_RESPONSES if wire == WIRE_RESPONSES else WIRE_COMPLETIONS), None if wire is None: if "gemini-" in tail: diff --git a/src/ucode/databricks.py b/src/ucode/databricks.py index 0d426819e..1ea21fe3e 100644 --- a/src/ucode/databricks.py +++ b/src/ucode/databricks.py @@ -2768,15 +2768,22 @@ def build_skills_mcp_url(workspace: str, locations: list[str]) -> str: # the `openai`, `azure_openai`, `microsoft_foundry`, `amazon_bedrock`, and # `bedrock_mantle` provider types all serve (the gateway fronts them with the # OpenAI surface); gemini speaks Google's, served by a Gemini Enterprise -# provider. copilot speaks Anthropic's for the Claude targets of anthropic and -# Bedrock services, and OpenAI's for openai and azure_openai services, on the -# gateway's native route for each API. Tags are the short form produced by `_provider_type_tag` +# provider. copilot speaks Anthropic's for the Claude targets of anthropic, Bedrock and +# microsoft_foundry services, and OpenAI's for the other targets of openai, azure_openai and +# microsoft_foundry services, on the gateway's native route for each API. Tags are the short form produced by `_provider_type_tag` # (e.g. `amazon_bedrock`). _TOOL_PROVIDER_TYPES: dict[str, tuple[str, ...]] = { "claude": ("anthropic", "amazon_bedrock", "bedrock_mantle"), "codex": ("openai", "azure_openai", "microsoft_foundry", "amazon_bedrock", "bedrock_mantle"), "gemini": ("gemini_enterprise",), - "copilot": ("anthropic", "amazon_bedrock", "bedrock_mantle", "openai", "azure_openai"), + "copilot": ( + "anthropic", + "amazon_bedrock", + "bedrock_mantle", + "openai", + "azure_openai", + "microsoft_foundry", + ), } # Agents that can only use a Bedrock service's Claude targets. @@ -2787,6 +2794,7 @@ def build_skills_mcp_url(workspace: str, locations: list[str]) -> str: # Mantle) instead of the agent's canonical model names, so ucode must pin them # explicitly. BEDROCK_PROVIDER_TYPES: tuple[str, ...] = ("amazon_bedrock", "bedrock_mantle") +MICROSOFT_FOUNDRY_PROVIDER_TYPE = "microsoft_foundry" # Provider types the gateway serves on its Anthropic Messages route. ANTHROPIC_API_PROVIDER_TYPES: tuple[str, ...] = ("anthropic", *BEDROCK_PROVIDER_TYPES) diff --git a/src/ucode/managed_setup.py b/src/ucode/managed_setup.py index 49c8d9910..5e9212e59 100644 --- a/src/ucode/managed_setup.py +++ b/src/ucode/managed_setup.py @@ -71,9 +71,9 @@ def supports_provider_service(tool: str, provider_type: str) -> bool: """True when ``tool`` can route through a ``provider_type`` Model Provider Service. Thin pass-through to :func:`ucode.databricks.tool_supports_provider_type` so the wizard has one - obvious place to ask. Only claude (anthropic / amazon_bedrock / bedrock_mantle) and codex - (openai / azure_openai / microsoft_foundry / amazon_bedrock / bedrock_mantle) have MPS support - today; the other harnesses are Databricks-hosted only. + obvious place to ask. Only claude (anthropic / amazon_bedrock / bedrock_mantle), codex + (openai / azure_openai / microsoft_foundry / amazon_bedrock / bedrock_mantle) and copilot + (all of those) have MPS support today; the other harnesses are Databricks-hosted only. """ return tool_supports_provider_type(tool, provider_type) diff --git a/tests/integration/README.md b/tests/integration/README.md index 75b848883..bcf4b24b7 100644 --- a/tests/integration/README.md +++ b/tests/integration/README.md @@ -273,8 +273,8 @@ fails before Copilot starts, and the TUI switches from Claude to GPT with `/model` mid-session. Model evidence comes from Copilot's own `~/.copilot/session-state/*/events.jsonl` (`utils/agents/copilot.py`). `test_ug_copilot_providers.py` runs Copilot through the MPS fixtures below: a file task on the -Anthropic service's model, on the OpenAI and Azure OpenAI services' `gpt-5-nano`, and on a named -model of the Bedrock allow-all service; launching on the relayed service fails before Copilot starts. +Anthropic service's model, on the OpenAI, Azure OpenAI and Microsoft Foundry services' `gpt-5-nano`, and +on a named model of the Bedrock allow-all service; launching on the relayed service fails before Copilot starts. The managed TTL journey installs `/etc/unitygateway/managed-settings.json` through real configure, checks root ownership and readable modes, then ages only user-owned `~/.ucode/refresh_ts.json`. @@ -516,7 +516,7 @@ test_ug_codex_headless.py # script prompts and model arguments test_ug_copilot_headless.py # Copilot via the session proxy: Claude + GPT tasks, no token on disk test_ug_copilot_lifecycle.py # Copilot refresh after revocation, sign-in expired, configure/revert, upgrade test_ug_copilot_models.py # Copilot model registry: Claude then GPT, unknown model, TUI /model switch -test_ug_copilot_providers.py # Copilot via Model Provider Services: Anthropic, OpenAI, Azure OpenAI, Bedrock allow-all; relayed refused +test_ug_copilot_providers.py # Copilot via Model Provider Services: Anthropic, OpenAI, Azure OpenAI, Foundry, Bedrock allow-all; relayed refused test_ug_codex_tracing.py # Codex OTLP export reaches the configured trace table test_ug_claude_commands.py # command help forwarding test_ug_codex_commands.py # command help and parser error forwarding @@ -651,6 +651,7 @@ MPS CUJs select the existing services already used by e2e: launch runs headless instead of opening a browser login. - Codex: `main.ucode.ci_openai_mps`, using its allowed `gpt-5-nano` model. - Codex and Copilot (Azure OpenAI): `main.ucode.ci_azure_openai_mps`, using its `gpt-5-nano` model. +- Codex and Copilot (Microsoft Foundry): `main.ucode.ci_foundry_mps`, using its `gpt-5-nano` model. Use `--claude-provider` / `--claude-relayed-provider` / `--codex-provider` to reproduce another existing service. Use `--claude-provider-model` / @@ -722,7 +723,7 @@ cases instead complete real file tasks through `--workspace` and `--model-locati journeys check setup and launch with existing services, without inference. There are **88 live cases** (including 12 marked TUI journeys) and **7 installation -checks** with Claude and Codex; selecting OpenCode adds one live headless case and two `managed_fixture` cases; selecting Copilot adds fifteen live cases. One +checks** with Claude and Codex; selecting OpenCode adds one live headless case and two `managed_fixture` cases; selecting Copilot adds sixteen live cases. One **`workspace_switch` case** uses two real workspaces and checks skills MCP cleanup and a completed Claude task. A further **44 `managed_fixture` cases** (twelve of them also `live`) run on the managed workspace with a checked-in JSON CodingAgentConfig from `tests/fixtures/managed_config/` @@ -948,7 +949,7 @@ A second non-blocking job, **Managed OpenCode self-managed** (`managed_fixture a in `test_ug_agents_self_managed.py`), installs Claude, Codex and OpenCode and runs against the managed e2e workspace (`E2E_ADMIN_WORKSPACE`) with the injected `managed_workspace_default` config, which enables Claude/Codex but not OpenCode. It is likewise outside `cujs`. -A non-blocking **Copilot** job (`live and copilot`, fourteen cases) runs on Linux and Windows the +A non-blocking **Copilot** job (`live and copilot`, fifteen cases) runs on Linux and Windows the same way and is likewise outside `cujs` until it is stable. Each lane installs only the agent CLIs it needs (one, except the managed OpenCode lane's diff --git a/tests/integration/test_ug_copilot_providers.py b/tests/integration/test_ug_copilot_providers.py index 01067331d..bd289b3d1 100644 --- a/tests/integration/test_ug_copilot_providers.py +++ b/tests/integration/test_ug_copilot_providers.py @@ -106,6 +106,30 @@ def test_ug_copilot_runs_a_task_through_an_azure_openai_provider( assert_no_session_registry_left(session) +def test_ug_copilot_runs_a_task_through_a_microsoft_foundry_provider( + live_session, workspace, codex_foundry_provider, codex_foundry_provider_model +): + """Scenario: configure Copilot, then run a headless task through a Microsoft Foundry provider + service on its GPT model. + + Expected: the session runs on `dbx-responses/` through the gateway's native OpenAI + route, the completed answer contains the fixture value and was sent as that model, and no + session registry is left. + """ + session = live_session + _configure(session, workspace) + session.record("copilot-provider.json", {"provider": codex_foundry_provider}) + + task, result = _run_task( + session, codex_foundry_provider, "--model", codex_foundry_provider_model + ) + + task.assert_headless_answer("copilot", result) + assert_completed_task_model(session, "copilot", task.value, codex_foundry_provider_model) + assert f"dbx-responses/{codex_foundry_provider_model}" in copilot_selected_models(session) + assert_no_session_registry_left(session) + + def test_ug_copilot_allow_all_bedrock_provider_runs_the_named_model( live_session, workspace, claude_bedrock_allow_all_provider, claude_bedrock_allow_all_model ): diff --git a/tests/test_agent_copilot.py b/tests/test_agent_copilot.py index fe624e1d9..ec9eb7cc2 100644 --- a/tests/test_agent_copilot.py +++ b/tests/test_agent_copilot.py @@ -336,6 +336,9 @@ class TestClassifyProviderModel: ("o3-mini", "openai", "responses"), ("gpt-4o", "azure_openai", "completions"), ("my-gpt4o-deployment", "azure_openai", "completions"), + ("claude-opus-4-8", "microsoft_foundry", "anthropic"), + ("gpt-5-nano", "microsoft_foundry", "responses"), + ("deepseek", "microsoft_foundry", "completions"), ], ) def test_provider_type_and_id_decide_the_wire(self, model, provider_type, wire): @@ -391,6 +394,20 @@ def test_keeps_usable_models_in_catalog_order_and_reports_the_rest(self): ("text-embedding-3-small", "it isn't a chat model (it serves openai/v1/embeddings)") ] + def test_foundry_claude_uses_anthropic_whatever_it_declares(self): + chat = ["openai/v1/chat/completions"] + models, skipped = provider_models( + ["deepseek", "claude-opus-4-8"], + "microsoft_foundry", + {"deepseek": chat, "claude-opus-4-8": chat}, + ) + + assert [(m.id, m.wire) for m in models] == [ + ("claude-opus-4-8", "anthropic"), + ("deepseek", "completions"), + ] + assert skipped == [] + def test_orders_dotted_provider_versions_newest_first(self): models, _ = provider_models( ["gpt-5", "gpt-5.6", "gpt-5.1", "gpt-5.5", "o1", "o4-mini", "o3"], "openai" diff --git a/tests/test_databricks.py b/tests/test_databricks.py index d8d9ec0eb..b0efa989f 100644 --- a/tests/test_databricks.py +++ b/tests/test_databricks.py @@ -1507,13 +1507,21 @@ def test_bedrock_claude_targets_required_only_for_claude(self, provider_type): assert not db_mod.service_usable_for_tool("copilot", service) @pytest.mark.parametrize( - "provider_type", ["anthropic", "amazon_bedrock", "bedrock_mantle", "openai", "azure_openai"] + "provider_type", + [ + "anthropic", + "amazon_bedrock", + "bedrock_mantle", + "openai", + "azure_openai", + "microsoft_foundry", + ], ) def test_copilot_routes_anthropic_and_openai_services(self, provider_type): service = {"provider_type": provider_type, "targets": [], "allow_all_targets": True} assert db_mod.service_usable_for_tool("copilot", service) - @pytest.mark.parametrize("provider_type", ["microsoft_foundry", "gemini_enterprise", "custom"]) + @pytest.mark.parametrize("provider_type", ["gemini_enterprise", "custom"]) def test_copilot_skips_other_provider_types(self, provider_type): service = {"provider_type": provider_type, "targets": [], "allow_all_targets": True} assert not db_mod.service_usable_for_tool("copilot", service) diff --git a/tests/test_managed_setup.py b/tests/test_managed_setup.py index c2a10c9a8..8350979dd 100644 --- a/tests/test_managed_setup.py +++ b/tests/test_managed_setup.py @@ -381,10 +381,16 @@ def test_codex_supports_openai_azure_foundry_and_bedrock(self): def test_claude_does_not_support_openai(self): assert not supports_provider_service("claude", "openai") - def test_copilot_supports_anthropic_openai_and_bedrock(self): - for provider_type in ("anthropic", "amazon_bedrock", "openai", "azure_openai"): + def test_copilot_supports_anthropic_openai_bedrock_and_foundry(self): + for provider_type in ( + "anthropic", + "amazon_bedrock", + "openai", + "azure_openai", + "microsoft_foundry", + ): assert supports_provider_service("copilot", provider_type), provider_type - assert not supports_provider_service("copilot", "microsoft_foundry") + assert not supports_provider_service("copilot", "gemini_enterprise") def test_other_agents_have_no_provider_support(self): for tool in ("gemini", "opencode", "pi"): From 18d3270170bff0c86b42805a56e12e8857091b19 Mon Sep 17 00:00:00 2001 From: Xi Lyu Date: Fri, 23 Oct 2026 16:00:11 +0000 Subject: [PATCH 2/2] copilot: point supports_provider_service at the provider-type table Co-authored-by: Isaac --- src/ucode/managed_setup.py | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/src/ucode/managed_setup.py b/src/ucode/managed_setup.py index 5e9212e59..6786c7519 100644 --- a/src/ucode/managed_setup.py +++ b/src/ucode/managed_setup.py @@ -70,10 +70,7 @@ def model_families_for_agent(tool: str) -> tuple[str, ...]: def supports_provider_service(tool: str, provider_type: str) -> bool: """True when ``tool`` can route through a ``provider_type`` Model Provider Service. - Thin pass-through to :func:`ucode.databricks.tool_supports_provider_type` so the wizard has one - obvious place to ask. Only claude (anthropic / amazon_bedrock / bedrock_mantle), codex - (openai / azure_openai / microsoft_foundry / amazon_bedrock / bedrock_mantle) and copilot - (all of those) have MPS support today; the other harnesses are Databricks-hosted only. + See :func:`ucode.databricks.tool_supports_provider_type`. """ return tool_supports_provider_type(tool, provider_type)