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
56 changes: 56 additions & 0 deletions litellm/llms/github_copilot/chat/transformation.py
Original file line number Diff line number Diff line change
Expand Up @@ -130,6 +130,62 @@ def get_supported_openai_params(self, model: str) -> list:

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.
"""
if "claude" in model.lower():
thinking = non_default_params.pop("thinking", None)
else:
thinking = None
existing_reasoning_effort = non_default_params.get(
"reasoning_effort"
) or optional_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") or 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"
Comment thread
greptile-apps[bot] marked this conversation as resolved.
Comment on lines +172 to +175

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 The "minimal" value is not part of the standard reasoning_effort enum recognised by OpenAI-compatible APIs (valid values are "low", "medium", "high"). GitHub Copilot proxies to an OpenAI-compatible endpoint, so sending "minimal" for any request whose budget_tokens falls below 2 000 will likely result in a 400/422 from the Copilot backend. Consider mapping the lowest tier to "low" instead, or verify that Copilot accepts "minimal" before shipping.

Suggested change
elif budget_tokens >= 2000:
reasoning_effort = "low"
else:
reasoning_effort = "minimal"
elif budget_tokens >= 2000:
reasoning_effort = "low"
else:
reasoning_effort = "low"

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

"minimal" is a valid reasoning_effort value, not an invalid one.

optional_params["reasoning_effort"] = reasoning_effort
elif (
"claude" in model.lower()
and non_default_params.get("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.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -433,6 +433,150 @@ 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_map_openai_params_thinking_does_not_overwrite_reasoning_effort_in_optional_params():
"""``reasoning_effort`` already in ``optional_params`` must not be overwritten
by the ``thinking`` conversion."""
config = GithubCopilotConfig()

optional_params = config.map_openai_params(
non_default_params={"thinking": {"type": "enabled", "budget_tokens": 15000}},
optional_params={"reasoning_effort": "low"},
model="claude-sonnet-4-20250514",
drop_params=False,
)
assert optional_params["reasoning_effort"] == "low"


def test_map_openai_params_thinking_not_popped_for_non_claude_model():
"""For non-Claude models, ``thinking`` must not be silently discarded —
it should pass through to the parent's unsupported-param handling."""
config = GithubCopilotConfig()

optional_params = config.map_openai_params(
non_default_params={"thinking": {"type": "enabled", "budget_tokens": 15000}},
optional_params={},
model="gpt-4o",
drop_params=False,
)
assert "reasoning_effort" not in optional_params


def test_map_openai_params_reasoning_effort_not_promoted_for_non_claude_model():
"""For non-Claude models, ``reasoning_effort`` must not be force-promoted
to ``optional_params``; the parent's supported-params filtering must run."""
config = GithubCopilotConfig()

optional_params = config.map_openai_params(
non_default_params={"reasoning_effort": "high"},
optional_params={},
model="gpt-4",
drop_params=True,
)
assert "reasoning_effort" not in optional_params


def test_copilot_vision_request_header_with_image():
"""Test that Copilot-Vision-Request header is added when messages contain images"""
config = GithubCopilotConfig()
Expand Down
Loading