fix(github_copilot): forward Claude thinking and reasoning_effort in map_openai_params

get_supported_openai_params advertised thinking and reasoning_effort for
extended-thinking Claude models, but map_openai_params was never overridden.
OpenAIConfig routes non-o-series/non-gpt-5 models to OpenAIGPTConfig, whose
supported-param whitelist contains neither key, so both were dropped during
mapping and never reached the Copilot API.

Fixes #25666
This commit is contained in:
liangliang_microsoft 2026-08-13 17:03:05 +08:00 committed by Allen Liang
parent 09889e1986
commit 1c1c96441f
2 changed files with 90 additions and 0 deletions

View file

@ -17,6 +17,8 @@ from ..common_utils import (
get_copilot_default_headers,
)
_CLAUDE_REASONING_PARAMS: Final = frozenset({"thinking", "reasoning_effort"})
class GithubCopilotConfig(OpenAIConfig):
def __init__(
@ -134,6 +136,26 @@ class GithubCopilotConfig(OpenAIConfig):
return base_params
def map_openai_params(
self,
non_default_params: dict,
optional_params: dict,
model: str,
drop_params: bool,
) -> dict:
# OpenAIConfig routes non-o-series/non-gpt-5 models to OpenAIGPTConfig, whose
# whitelist has neither key, so advertising them alone dropped them (#25666)
supported: Final = frozenset(self.get_supported_openai_params(model)) & _CLAUDE_REASONING_PARAMS
claude_params: Final = {k: v for k, v in non_default_params.items() if k in supported}
remaining: Final = {k: v for k, v in non_default_params.items() if k not in supported}
return super().map_openai_params(
non_default_params=remaining,
optional_params={**optional_params, **claude_params},
model=model,
drop_params=drop_params,
)
def _determine_initiator(self, messages: list[AllMessageValues]) -> str:
"""
Determine if request is user or agent initiated based on message roles.

View file

@ -982,3 +982,71 @@ def test_openai_handler_repairs_github_copilot_empty_choices(
assert result.choices[0].message.content == "Hi there"
assert result.choices[0].finish_reason == "stop"
mock_request.assert_called_once()
def test_map_openai_params_forwards_reasoning_effort_for_claude():
"""Claude reasoning params must survive map_openai_params, not just be advertised.
Regression test for the gap behind #25666: GithubCopilotConfig overrode
get_supported_openai_params() to advertise ``thinking``/``reasoning_effort``
for extended-thinking Claude models, but never overrode map_openai_params().
Mapping therefore fell through to OpenAIConfig -> OpenAIGPTConfig, whose
supported-param whitelist has neither key, so both were silently discarded
and never reached the Copilot API. The advertise-only assertions in
test_get_supported_openai_params_claude_model passed the whole time, because
they never inspect the mapped output.
"""
config = GithubCopilotConfig()
model = "claude-sonnet-4-20250514"
# Guard the premise: the param is advertised as supported for this model.
assert "reasoning_effort" in config.get_supported_openai_params(model)
mapped = config.map_openai_params(
non_default_params={"reasoning_effort": "high"},
optional_params={},
model=model,
drop_params=False,
)
assert mapped.get("reasoning_effort") == "high"
mapped_thinking = config.map_openai_params(
non_default_params={"thinking": {"type": "enabled", "budget_tokens": 4096}},
optional_params={},
model=model,
drop_params=False,
)
assert mapped_thinking.get("thinking") == {
"type": "enabled",
"budget_tokens": 4096,
}
def test_map_openai_params_does_not_forward_reasoning_for_unsupported_claude():
"""A Claude model without extended thinking must not gain reasoning params."""
config = GithubCopilotConfig()
model = "claude-3.5-sonnet"
assert "reasoning_effort" not in config.get_supported_openai_params(model)
mapped = config.map_openai_params(
non_default_params={"reasoning_effort": "high"},
optional_params={},
model=model,
drop_params=True,
)
assert "reasoning_effort" not in mapped
def test_map_openai_params_preserves_standard_openai_params_for_claude():
"""The Claude passthrough must not regress ordinary OpenAI param mapping."""
config = GithubCopilotConfig()
mapped = config.map_openai_params(
non_default_params={"temperature": 0.5, "max_tokens": 128},
optional_params={},
model="claude-sonnet-4-20250514",
drop_params=False,
)
assert mapped.get("temperature") == 0.5
assert mapped.get("max_tokens") == 128