From 1c1c96441fe2f44f04d7a90e161d5f1177a86d98 Mon Sep 17 00:00:00 2001 From: liangliang_microsoft Date: Thu, 13 Aug 2026 17:03:05 +0800 Subject: [PATCH 1/4] 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 --- .../github_copilot/chat/transformation.py | 22 ++++++ .../test_github_copilot_transformation.py | 68 +++++++++++++++++++ 2 files changed, 90 insertions(+) diff --git a/litellm/llms/github_copilot/chat/transformation.py b/litellm/llms/github_copilot/chat/transformation.py index 27a0028ce4a..0536782ad09 100644 --- a/litellm/llms/github_copilot/chat/transformation.py +++ b/litellm/llms/github_copilot/chat/transformation.py @@ -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. diff --git a/tests/test_litellm/llms/github_copilot/test_github_copilot_transformation.py b/tests/test_litellm/llms/github_copilot/test_github_copilot_transformation.py index f69ba7df938..fdffdc83541 100644 --- a/tests/test_litellm/llms/github_copilot/test_github_copilot_transformation.py +++ b/tests/test_litellm/llms/github_copilot/test_github_copilot_transformation.py @@ -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 From 8aa15d6d75e9c96b57f3364ffcdb0d244cffd80b Mon Sep 17 00:00:00 2001 From: allenliang2022 Date: Thu, 13 Aug 2026 17:54:30 +0800 Subject: [PATCH 2/4] fix(github_copilot): gate reasoning params on capability, not on the model id Copilot also serves Gemini, Grok and MAI models that advertise reasoning_effort, but the param was only forwarded when the model id contained "claude", so every other family had it dropped despite the API accepting it. thinking stays Claude-only since it is Anthropic-native. --- .../github_copilot/chat/transformation.py | 31 +++++++------- .../test_github_copilot_transformation.py | 42 +++++++++++++++++++ 2 files changed, 57 insertions(+), 16 deletions(-) diff --git a/litellm/llms/github_copilot/chat/transformation.py b/litellm/llms/github_copilot/chat/transformation.py index 0536782ad09..d74a478882f 100644 --- a/litellm/llms/github_copilot/chat/transformation.py +++ b/litellm/llms/github_copilot/chat/transformation.py @@ -17,7 +17,7 @@ from ..common_utils import ( get_copilot_default_headers, ) -_CLAUDE_REASONING_PARAMS: Final = frozenset({"thinking", "reasoning_effort"}) +_REASONING_PARAMS: Final = frozenset({"thinking", "reasoning_effort"}) class GithubCopilotConfig(OpenAIConfig): @@ -116,23 +116,22 @@ class GithubCopilotConfig(OpenAIConfig): """ Get supported OpenAI parameters for GitHub Copilot. - For Claude models that support extended thinking (Claude 4 family and Claude 3-7), includes thinking and reasoning_effort parameters. - For other models, returns standard OpenAI parameters (which may include reasoning_effort for o-series models). + Claude models that support extended thinking accept both thinking and reasoning_effort. + Other reasoning-capable Copilot models (Gemini, Grok, gpt-5 family) accept reasoning_effort only. """ from litellm.utils import supports_reasoning - # Get base OpenAI parameters base_params: Final = super().get_supported_openai_params(model) + normalized: Final = model.lower() - # Add Claude-specific parameters for models that support extended thinking - if "claude" in model.lower() and supports_reasoning( - model=model.lower(), - ): - if "thinking" not in base_params: - base_params.append("thinking") - # reasoning_effort is not included by parent for Claude models, so add it - if "reasoning_effort" not in base_params: - base_params.append("reasoning_effort") + if not supports_reasoning(model=normalized): + return base_params + + # thinking is Anthropic-native; only Claude accepts it + if "claude" in normalized and "thinking" not in base_params: + base_params.append("thinking") + if "reasoning_effort" not in base_params: + base_params.append("reasoning_effort") return base_params @@ -145,13 +144,13 @@ class GithubCopilotConfig(OpenAIConfig): ) -> 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} + supported: Final = frozenset(self.get_supported_openai_params(model)) & _REASONING_PARAMS + reasoning_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}, + optional_params={**optional_params, **reasoning_params}, model=model, drop_params=drop_params, ) diff --git a/tests/test_litellm/llms/github_copilot/test_github_copilot_transformation.py b/tests/test_litellm/llms/github_copilot/test_github_copilot_transformation.py index fdffdc83541..d97be9fb161 100644 --- a/tests/test_litellm/llms/github_copilot/test_github_copilot_transformation.py +++ b/tests/test_litellm/llms/github_copilot/test_github_copilot_transformation.py @@ -1050,3 +1050,45 @@ def test_map_openai_params_preserves_standard_openai_params_for_claude(): ) assert mapped.get("temperature") == 0.5 assert mapped.get("max_tokens") == 128 + + +def test_non_claude_reasoning_model_forwards_reasoning_effort(): + """Reasoning-capable non-Claude Copilot models must also keep reasoning_effort. + + Copilot serves Gemini, Grok and MAI models that advertise reasoning_effort in + its own model catalog, but the provider previously gated the param on the + model id containing "claude", so every other family had it dropped while the + upstream API would have accepted it. + """ + config = GithubCopilotConfig() + model = "gemini-2.5-pro" + + with patch( + "litellm.utils.supports_reasoning", + return_value=True, + ): + supported = config.get_supported_openai_params(model) + assert "reasoning_effort" in supported + # thinking is Anthropic-native and must stay Claude-only + assert "thinking" not in supported + + mapped = config.map_openai_params( + non_default_params={"reasoning_effort": "high"}, + optional_params={}, + model=model, + drop_params=False, + ) + assert mapped.get("reasoning_effort") == "high" + + +def test_non_reasoning_model_keeps_reasoning_effort_out(): + """A model the catalog reports as non-reasoning must not gain the param.""" + config = GithubCopilotConfig() + + with patch( + "litellm.utils.supports_reasoning", + return_value=False, + ): + supported = config.get_supported_openai_params("gpt-4o") + assert "reasoning_effort" not in supported + assert "thinking" not in supported From 4648fcba4fada62af87a6482bb843d5ac039863e Mon Sep 17 00:00:00 2001 From: allenliang2022 Date: Thu, 13 Aug 2026 18:30:43 +0800 Subject: [PATCH 3/4] test(github_copilot): trim regression docstrings and annotate locals --- .../test_github_copilot_transformation.py | 53 +++++++------------ 1 file changed, 18 insertions(+), 35 deletions(-) diff --git a/tests/test_litellm/llms/github_copilot/test_github_copilot_transformation.py b/tests/test_litellm/llms/github_copilot/test_github_copilot_transformation.py index d97be9fb161..17bfc9ad2ce 100644 --- a/tests/test_litellm/llms/github_copilot/test_github_copilot_transformation.py +++ b/tests/test_litellm/llms/github_copilot/test_github_copilot_transformation.py @@ -3,7 +3,7 @@ import json import os import sys from datetime import datetime, timedelta -from typing import AsyncGenerator +from typing import AsyncGenerator, Final from unittest.mock import AsyncMock, MagicMock, mock_open, patch import pytest @@ -985,24 +985,13 @@ def test_openai_handler_repairs_github_copilot_empty_choices( def test_map_openai_params_forwards_reasoning_effort_for_claude(): - """Claude reasoning params must survive map_openai_params, not just be advertised. + """Claude reasoning params must survive mapping, not just be advertised (#25666).""" + config: Final = GithubCopilotConfig() + model: Final = "claude-sonnet-4-20250514" - 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( + mapped: Final = config.map_openai_params( non_default_params={"reasoning_effort": "high"}, optional_params={}, model=model, @@ -1010,7 +999,7 @@ def test_map_openai_params_forwards_reasoning_effort_for_claude(): ) assert mapped.get("reasoning_effort") == "high" - mapped_thinking = config.map_openai_params( + mapped_thinking: Final = config.map_openai_params( non_default_params={"thinking": {"type": "enabled", "budget_tokens": 4096}}, optional_params={}, model=model, @@ -1024,12 +1013,12 @@ def test_map_openai_params_forwards_reasoning_effort_for_claude(): 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" + config: Final = GithubCopilotConfig() + model: Final = "claude-3.5-sonnet" assert "reasoning_effort" not in config.get_supported_openai_params(model) - mapped = config.map_openai_params( + mapped: Final = config.map_openai_params( non_default_params={"reasoning_effort": "high"}, optional_params={}, model=model, @@ -1040,9 +1029,9 @@ def test_map_openai_params_does_not_forward_reasoning_for_unsupported_claude(): def test_map_openai_params_preserves_standard_openai_params_for_claude(): """The Claude passthrough must not regress ordinary OpenAI param mapping.""" - config = GithubCopilotConfig() + config: Final = GithubCopilotConfig() - mapped = config.map_openai_params( + mapped: Final = config.map_openai_params( non_default_params={"temperature": 0.5, "max_tokens": 128}, optional_params={}, model="claude-sonnet-4-20250514", @@ -1053,26 +1042,20 @@ def test_map_openai_params_preserves_standard_openai_params_for_claude(): def test_non_claude_reasoning_model_forwards_reasoning_effort(): - """Reasoning-capable non-Claude Copilot models must also keep reasoning_effort. - - Copilot serves Gemini, Grok and MAI models that advertise reasoning_effort in - its own model catalog, but the provider previously gated the param on the - model id containing "claude", so every other family had it dropped while the - upstream API would have accepted it. - """ - config = GithubCopilotConfig() - model = "gemini-2.5-pro" + """Reasoning-capable non-Claude Copilot models must also keep reasoning_effort.""" + config: Final = GithubCopilotConfig() + model: Final = "gemini-2.5-pro" with patch( "litellm.utils.supports_reasoning", return_value=True, ): - supported = config.get_supported_openai_params(model) + supported: Final = config.get_supported_openai_params(model) assert "reasoning_effort" in supported # thinking is Anthropic-native and must stay Claude-only assert "thinking" not in supported - mapped = config.map_openai_params( + mapped: Final = config.map_openai_params( non_default_params={"reasoning_effort": "high"}, optional_params={}, model=model, @@ -1083,12 +1066,12 @@ def test_non_claude_reasoning_model_forwards_reasoning_effort(): def test_non_reasoning_model_keeps_reasoning_effort_out(): """A model the catalog reports as non-reasoning must not gain the param.""" - config = GithubCopilotConfig() + config: Final = GithubCopilotConfig() with patch( "litellm.utils.supports_reasoning", return_value=False, ): - supported = config.get_supported_openai_params("gpt-4o") + supported: Final = config.get_supported_openai_params("gpt-4o") assert "reasoning_effort" not in supported assert "thinking" not in supported From 6f614c484b36ca492c565152b961468dbd8db126 Mon Sep 17 00:00:00 2001 From: allenliang2022 Date: Thu, 13 Aug 2026 19:13:04 +0800 Subject: [PATCH 4/4] fix(github_copilot): explain the inherited dict annotations for the type gate --- litellm/llms/github_copilot/chat/transformation.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/litellm/llms/github_copilot/chat/transformation.py b/litellm/llms/github_copilot/chat/transformation.py index d74a478882f..4ceb7338c0e 100644 --- a/litellm/llms/github_copilot/chat/transformation.py +++ b/litellm/llms/github_copilot/chat/transformation.py @@ -137,11 +137,11 @@ class GithubCopilotConfig(OpenAIConfig): def map_openai_params( self, - non_default_params: dict, - optional_params: dict, + non_default_params: dict, # mutable-ok: matches the BaseConfig override signature + optional_params: dict, # mutable-ok: matches the BaseConfig override signature model: str, drop_params: bool, - ) -> dict: + ) -> dict: # mutable-ok: matches the BaseConfig override signature # 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)) & _REASONING_PARAMS