fix(transformation): convert 'thinking' parameter to 'reasoning_effort' for Claude models

This commit is contained in:
Timothy Oluwatobi Ojebiyi 2026-05-15 15:06:58 +01:00
parent bcbae93ecc
commit 3c378f4bfe
2 changed files with 150 additions and 0 deletions

View file

@ -130,6 +130,54 @@ class GithubCopilotConfig(OpenAIConfig):
return base_params
def map_openai_params(
self,
non_default_params: dict,
optional_params: dict,
model: str,
drop_params: bool,
) -> dict:
"""
Map OpenAI params to GitHub Copilot params.
GitHub Copilot uses an OpenAI-compatible API and does not understand the
Anthropic-native ``thinking`` parameter. When Claude Code calls the proxy's
``/v1/messages`` endpoint with ``thinking`` and the request is routed to a
``github_copilot/claude-*`` model, ``thinking`` must be converted to
``reasoning_effort`` before being forwarded to the Copilot API.
``reasoning_effort`` is written directly to ``optional_params`` rather than
deferred to the parent: ``OpenAIConfig.map_openai_params`` dispatches to the
global ``openAIGPTConfig`` whose supported-params list does not include
``reasoning_effort`` for Claude models, so a deferred write would be dropped.
"""
thinking = non_default_params.pop("thinking", None)
existing_reasoning_effort = non_default_params.get("reasoning_effort")
if (
thinking is not None
and isinstance(thinking, dict)
and thinking.get("type") == "enabled"
and existing_reasoning_effort is None
):
budget_tokens = thinking.get("budget_tokens", 0)
if budget_tokens >= 10000:
reasoning_effort = "high"
elif budget_tokens >= 5000:
reasoning_effort = "medium"
elif budget_tokens >= 2000:
reasoning_effort = "low"
else:
reasoning_effort = "minimal"
optional_params["reasoning_effort"] = reasoning_effort
elif existing_reasoning_effort is not None:
optional_params["reasoning_effort"] = non_default_params.pop(
"reasoning_effort"
)
return super().map_openai_params(
non_default_params, optional_params, model, drop_params
)
def _determine_initiator(self, messages: List[AllMessageValues]) -> str:
"""
Determine if request is user or agent initiated based on message roles.

View file

@ -433,6 +433,108 @@ def test_get_supported_openai_params_case_insensitive():
assert "reasoning_effort" not in supported_params_35
def test_map_openai_params_converts_thinking_to_reasoning_effort():
"""
Test that Anthropic-native ``thinking`` is converted to ``reasoning_effort`` for
GitHub Copilot Claude models.
GitHub Copilot uses an OpenAI-compatible API and does not understand the
Anthropic ``thinking`` parameter. When Claude Code sends ``thinking`` via the
proxy's ``/v1/messages`` endpoint, litellm must translate it to
``reasoning_effort`` before forwarding to Copilot.
"""
config = GithubCopilotConfig()
model = "claude-sonnet-4-20250514"
# budget_tokens < 2000 -> "minimal"
optional_params = config.map_openai_params(
non_default_params={"thinking": {"type": "enabled", "budget_tokens": 1024}},
optional_params={},
model=model,
drop_params=False,
)
assert "thinking" not in optional_params
assert optional_params["reasoning_effort"] == "minimal"
# 2000 <= budget_tokens < 5000 -> "low"
optional_params = config.map_openai_params(
non_default_params={"thinking": {"type": "enabled", "budget_tokens": 2000}},
optional_params={},
model=model,
drop_params=False,
)
assert "thinking" not in optional_params
assert optional_params["reasoning_effort"] == "low"
# 5000 <= budget_tokens < 10000 -> "medium"
optional_params = config.map_openai_params(
non_default_params={"thinking": {"type": "enabled", "budget_tokens": 5000}},
optional_params={},
model=model,
drop_params=False,
)
assert "thinking" not in optional_params
assert optional_params["reasoning_effort"] == "medium"
# budget_tokens >= 10000 -> "high"
optional_params = config.map_openai_params(
non_default_params={"thinking": {"type": "enabled", "budget_tokens": 15000}},
optional_params={},
model=model,
drop_params=False,
)
assert "thinking" not in optional_params
assert optional_params["reasoning_effort"] == "high"
def test_map_openai_params_thinking_does_not_overwrite_existing_reasoning_effort():
"""Caller-supplied ``reasoning_effort`` must not be overwritten by the
``thinking`` translation."""
config = GithubCopilotConfig()
optional_params = config.map_openai_params(
non_default_params={
"thinking": {"type": "enabled", "budget_tokens": 15000},
"reasoning_effort": "low",
},
optional_params={},
model="claude-sonnet-4-20250514",
drop_params=False,
)
assert "thinking" not in optional_params
assert optional_params["reasoning_effort"] == "low"
def test_map_openai_params_disabled_thinking_is_dropped():
"""``thinking`` with ``type=disabled`` should be dropped without producing
``reasoning_effort``."""
config = GithubCopilotConfig()
optional_params = config.map_openai_params(
non_default_params={"thinking": {"type": "disabled"}},
optional_params={},
model="claude-sonnet-4-20250514",
drop_params=False,
)
assert "thinking" not in optional_params
assert "reasoning_effort" not in optional_params
def test_map_openai_params_no_thinking_leaves_params_unchanged():
"""A request without ``thinking`` should not gain a ``reasoning_effort`` value."""
config = GithubCopilotConfig()
optional_params = config.map_openai_params(
non_default_params={"temperature": 0.5},
optional_params={},
model="claude-sonnet-4-20250514",
drop_params=False,
)
assert "thinking" not in optional_params
assert "reasoning_effort" not in optional_params
assert optional_params.get("temperature") == 0.5
def test_copilot_vision_request_header_with_image():
"""Test that Copilot-Vision-Request header is added when messages contain images"""
config = GithubCopilotConfig()