This commit is contained in:
allenliang2022 2026-08-27 12:45:00 -07:00 • committed by GitHub
commit ac57e3bfe2
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
2 changed files with 127 additions and 13 deletions

View file

@ -17,6 +17,8 @@ from ..common_utils import (
get_copilot_default_headers,
)
_REASONING_PARAMS: Final = frozenset({"thinking", "reasoning_effort"})
class GithubCopilotConfig(OpenAIConfig):
def __init__(
@ -114,26 +116,45 @@ 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
def map_openai_params(
self,
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: # 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
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, **reasoning_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

@ -1,7 +1,7 @@
import asyncio
import json
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
@ -978,3 +978,96 @@ 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 mapping, not just be advertised (#25666)."""
config: Final = GithubCopilotConfig()
model: Final = "claude-sonnet-4-20250514"
assert "reasoning_effort" in config.get_supported_openai_params(model)
mapped: Final = 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: Final = 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: Final = GithubCopilotConfig()
model: Final = "claude-3.5-sonnet"
assert "reasoning_effort" not in config.get_supported_openai_params(model)
mapped: Final = 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: Final = GithubCopilotConfig()
mapped: Final = 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
def test_non_claude_reasoning_model_forwards_reasoning_effort():
"""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: 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: Final = 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: Final = GithubCopilotConfig()
with patch(
"litellm.utils.supports_reasoning",
return_value=False,
):
supported: Final = config.get_supported_openai_params("gpt-4o")
assert "reasoning_effort" not in supported
assert "thinking" not in supported