Skip to content
Closed
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
22 changes: 19 additions & 3 deletions plugins/model-providers/copilot/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -35,9 +35,25 @@ def build_api_kwargs_extras(
supported_efforts = github_model_reasoning_efforts(model)
if supported_efforts and reasoning_config:
effort = reasoning_config.get("effort", "medium")
# Normalize stronger generic levels to the nearest supported.
if effort in {"xhigh", "max", "ultra"}:
effort = "high"
# Honor the requested level when the live Copilot catalog
# lists it as supported: gpt-5.5/gpt-5.4 DO support
# ``xhigh``. Only downgrade levels the catalog does NOT
# list (e.g. ``xhigh``/``max`` on models capped lower, or
# ``minimal`` where unsupported), choosing the nearest
# weaker supported level rather than forwarding verbatim.
#
# (Previously this unconditionally mapped xhigh->high, a
# stale guard that silently capped models which do support
# the higher level.)
if effort not in supported_efforts:
if effort == "xhigh" and "high" in supported_efforts:
effort = "high"
elif effort == "minimal" and "low" in supported_efforts:
effort = "low"
elif "medium" in supported_efforts:
effort = "medium"
else:
effort = supported_efforts[0]
if effort in supported_efforts:
extra_body["reasoning"] = {"effort": effort}
elif supported_efforts:
Expand Down
2 changes: 1 addition & 1 deletion run_agent.py
Original file line number Diff line number Diff line change
Expand Up @@ -5451,7 +5451,7 @@ def _github_models_reasoning_extra_body(self) -> dict | None:
else:
requested_effort = "medium"

if requested_effort == "xhigh" and "high" in supported_efforts:
if requested_effort == "xhigh" and "xhigh" not in supported_efforts and "high" in supported_efforts:
requested_effort = "high"
elif requested_effort not in supported_efforts:
if requested_effort == "minimal" and "low" in supported_efforts:
Expand Down
102 changes: 102 additions & 0 deletions tests/plugins/model_providers/test_copilot_profile.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,102 @@
"""Unit tests for the Copilot provider profile's reasoning-effort wiring.

GitHub Copilot serves different models with different supported reasoning-effort
sets (the live ``/models`` catalog reports them per model). The profile must
forward the requested effort when the catalog lists it as supported, and only
downgrade to the nearest weaker supported level when it does not, rather than
unconditionally collapsing ``xhigh`` to ``high`` (which silently capped models
that actually support the higher level).

These tests pin that contract without going live, by stubbing the catalog
lookup ``github_model_reasoning_efforts``.
"""

from __future__ import annotations

import pytest


@pytest.fixture
def copilot_profile():
"""Resolve the registered Copilot profile.

Importing ``model_tools`` triggers plugin discovery, which registers the
Copilot profile. Going through ``get_provider_profile`` keeps the test
honest: if the registered class is ever swapped for a plain
``ProviderProfile`` the assertions below collapse.
"""
import model_tools # noqa: F401
import providers

profile = providers.get_provider_profile("copilot")
assert profile is not None, "copilot provider profile must be registered"
return profile


def _patch_efforts(monkeypatch, efforts):
"""Stub the catalog lookup the profile calls for supported efforts."""
import hermes_cli.models as models_mod
monkeypatch.setattr(
models_mod, "github_model_reasoning_efforts", lambda model: list(efforts)
)


class TestCopilotReasoningEffortClamp:
def test_supported_effort_forwarded_verbatim(self, copilot_profile, monkeypatch):
"""xhigh is forwarded unchanged when the catalog lists it."""
_patch_efforts(monkeypatch, ["minimal", "low", "medium", "high", "xhigh"])
extra_body, _ = copilot_profile.build_api_kwargs_extras(
model="gpt-5.5",
reasoning_config={"effort": "xhigh"},
supports_reasoning=True,
)
assert extra_body["reasoning"] == {"effort": "xhigh"}

def test_xhigh_downgrades_to_high_when_unsupported(self, copilot_profile, monkeypatch):
"""A model whose catalog lacks xhigh gets the nearest weaker level."""
_patch_efforts(monkeypatch, ["low", "medium", "high"])
extra_body, _ = copilot_profile.build_api_kwargs_extras(
model="o-series-model",
reasoning_config={"effort": "xhigh"},
supports_reasoning=True,
)
assert extra_body["reasoning"] == {"effort": "high"}

def test_minimal_downgrades_to_low_when_unsupported(self, copilot_profile, monkeypatch):
_patch_efforts(monkeypatch, ["low", "medium", "high"])
extra_body, _ = copilot_profile.build_api_kwargs_extras(
model="o-series-model",
reasoning_config={"effort": "minimal"},
supports_reasoning=True,
)
assert extra_body["reasoning"] == {"effort": "low"}

def test_unsupported_effort_falls_back_to_medium(self, copilot_profile, monkeypatch):
"""An effort not in the set, with no specific rule, falls to medium."""
_patch_efforts(monkeypatch, ["low", "medium", "high"])
extra_body, _ = copilot_profile.build_api_kwargs_extras(
model="some-model",
reasoning_config={"effort": "garbage"},
supports_reasoning=True,
)
assert extra_body["reasoning"] == {"effort": "medium"}

def test_falls_back_to_first_supported_when_no_medium(self, copilot_profile, monkeypatch):
"""If medium isn't supported either, pick the first supported level."""
_patch_efforts(monkeypatch, ["low", "high"])
extra_body, _ = copilot_profile.build_api_kwargs_extras(
model="weird-model",
reasoning_config={"effort": "xhigh"},
supports_reasoning=True,
)
# xhigh not supported, high IS supported → high wins via the xhigh rule.
assert extra_body["reasoning"] == {"effort": "high"}

def test_first_supported_when_no_rule_matches(self, copilot_profile, monkeypatch):
_patch_efforts(monkeypatch, ["low", "high"])
extra_body, _ = copilot_profile.build_api_kwargs_extras(
model="weird-model",
reasoning_config={"effort": "garbage"},
supports_reasoning=True,
)
assert extra_body["reasoning"] == {"effort": "low"}
23 changes: 19 additions & 4 deletions tests/run_agent/test_run_agent.py
Original file line number Diff line number Diff line change
Expand Up @@ -1900,23 +1900,38 @@ def test_reasoning_sent_for_copilot_gpt5(self, agent):
)
assert kwargs["extra_body"]["reasoning"] == {"effort": "medium"}

def test_reasoning_xhigh_normalized_for_copilot(self, agent):
"""xhigh effort should normalize to high for Copilot GitHub Models."""
def test_reasoning_xhigh_preserved_for_copilot_when_supported(self, agent, monkeypatch):
"""The registered Copilot profile must preserve a supported xhigh."""
from agent.transports import get_transport
from providers import get_provider_profile

monkeypatch.setattr(
"hermes_cli.models.github_model_reasoning_efforts",
lambda _model: ["none", "low", "medium", "high", "xhigh"],
)
transport = get_transport("chat_completions")
profile = get_provider_profile("copilot")
msgs = [{"role": "user", "content": "hi"}]
kwargs = transport.build_kwargs(
model="gpt-5.4",
model="gpt-5.5",
messages=msgs,
tools=None,
supports_reasoning=True,
reasoning_config={"enabled": True, "effort": "xhigh"},
provider_profile=profile,
)
assert kwargs["extra_body"]["reasoning"] == {"effort": "high"}
assert kwargs["extra_body"]["reasoning"] == {"effort": "xhigh"}

def test_core_responses_preserves_supported_xhigh(self, agent, monkeypatch):
"""The core GitHub Responses path must preserve a supported xhigh."""
monkeypatch.setattr(
"hermes_cli.models.github_model_reasoning_efforts",
lambda _model: ["none", "low", "medium", "high", "xhigh"],
)
agent.model = "gpt-5.5"
agent.reasoning_config = {"enabled": True, "effort": "xhigh"}

assert agent._github_models_reasoning_extra_body() == {"effort": "xhigh"}

def test_reasoning_omitted_for_non_reasoning_copilot_model(self, agent):
agent.base_url = "https://api.githubcopilot.com"
Expand Down