Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -91,7 +91,7 @@ restart `ug copilot`. Proxy errors are logged to `~/.ucode/logs/copilot-proxy.lo

`ug copilot --provider <catalog>.<schema>.<name>` 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
Expand Down
8 changes: 7 additions & 1 deletion src/ucode/agents/copilot.py
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down Expand Up @@ -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:
Expand All @@ -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
Comment thread
xi-db marked this conversation as resolved.
return (WIRE_RESPONSES if wire == WIRE_RESPONSES else WIRE_COMPLETIONS), None
if wire is None:
if "gemini-" in tail:
Expand Down
16 changes: 12 additions & 4 deletions src/ucode/databricks.py
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand All @@ -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)

Expand Down
5 changes: 1 addition & 4 deletions src/ucode/managed_setup.py
Original file line number Diff line number Diff line change
Expand Up @@ -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) and codex
(openai / azure_openai / microsoft_foundry / amazon_bedrock / bedrock_mantle) 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)

Expand Down
11 changes: 6 additions & 5 deletions tests/integration/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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`.
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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` /
Expand Down Expand Up @@ -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/`
Expand Down Expand Up @@ -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
Expand Down
24 changes: 24 additions & 0 deletions tests/integration/test_ug_copilot_providers.py
Original file line number Diff line number Diff line change
Expand Up @@ -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/<model>` 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
):
Expand Down
17 changes: 17 additions & 0 deletions tests/test_agent_copilot.py
Original file line number Diff line number Diff line change
Expand Up @@ -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):
Expand Down Expand Up @@ -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"
Expand Down
12 changes: 10 additions & 2 deletions tests/test_databricks.py
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down
12 changes: 9 additions & 3 deletions tests/test_managed_setup.py
Original file line number Diff line number Diff line change
Expand Up @@ -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"):
Expand Down
Loading