From 07e8f49d366f7ec27f585b14dc753e094746d82f Mon Sep 17 00:00:00 2001 From: codgician <15964984+codgician@users.noreply.github.com> Date: Sat, 2 May 2026 11:37:30 +0800 Subject: [PATCH] fix(github-copilot): route per-model on /v1/responses based on model info --- .../responses/transformation.py | 48 +++ litellm/utils.py | 8 +- model_prices_and_context_window.json | 324 ++++++++++-------- ...github_copilot_responses_transformation.py | 190 +++++++++- 4 files changed, 416 insertions(+), 154 deletions(-) diff --git a/litellm/llms/github_copilot/responses/transformation.py b/litellm/llms/github_copilot/responses/transformation.py index 0929f95cf43..050d4a5f2aa 100644 --- a/litellm/llms/github_copilot/responses/transformation.py +++ b/litellm/llms/github_copilot/responses/transformation.py @@ -22,6 +22,7 @@ ) from litellm.types.router import GenericLiteLLMParams from litellm.types.utils import LlmProviders +from litellm.utils import _get_model_info_helper from ..authenticator import Authenticator from ..common_utils import ( @@ -38,6 +39,53 @@ LiteLLMLoggingObj = Any +def github_copilot_supports_responses_api(model: str) -> bool: + """ + Resolve whether to use the native Responses API for a github_copilot model. + + Copilot's /v1/responses endpoint is per-model (only some models like + gpt-5.5, gpt-5.4, gpt-5.4-mini opt in upstream). The Responses API config + is registered for github_copilot provider-wide, so this function gates + per-model to keep /v1/responses calls from failing upstream on chat-only + Copilot models like claude-opus-4.7 or gemini-3.1-pro-preview. + + The router calls ``litellm.register_model`` for every proxy deployment, + which merges the user's per-deployment ``model_info`` (e.g. ``mode: chat`` + to force a chat-only override) into ``litellm.model_cost`` before any + request runs. ``_get_model_info_helper`` therefore returns merged data + with user overrides already applied. + + Resolution order (first match wins): + 1. ``mode == "responses"`` → True (positive opt-in; user or catalog). + 2. ``mode == "chat"`` → False (explicit opt-out wins over endpoint + declarations, letting users force the bridge for dual-endpoint models). + 3. ``"/v1/responses"`` in ``supported_endpoints`` → True. + 4. Otherwise → False (conservative default; the bridge always works + because every Copilot model supports /chat/completions). + + Catalog lookup raising (model not registered) → False (conservative). + """ + try: + info = _get_model_info_helper(model=model, custom_llm_provider="github_copilot") + except Exception as e: + verbose_logger.debug( + "github_copilot_supports_responses_api: get_model_info failed " + "for %s: %s", + model, + e, + ) + return False + + mode = info.get("mode") + if mode == "responses": + return True + if mode == "chat": + return False + + endpoints = info.get("supported_endpoints") + return isinstance(endpoints, list) and "/v1/responses" in endpoints + + class GithubCopilotResponsesAPIConfig(OpenAIResponsesAPIConfig): """ Configuration for GitHub Copilot's Responses API. diff --git a/litellm/utils.py b/litellm/utils.py index 027c9fedced..bd2410e66e8 100644 --- a/litellm/utils.py +++ b/litellm/utils.py @@ -8575,7 +8575,13 @@ def _get_python_responses_api_config( elif litellm.LlmProviders.XAI == provider: return litellm.XAIResponsesAPIConfig() elif litellm.LlmProviders.GITHUB_COPILOT == provider: - return litellm.GithubCopilotResponsesAPIConfig() + from litellm.llms.github_copilot.responses.transformation import ( + github_copilot_supports_responses_api, + ) + + if model is None or github_copilot_supports_responses_api(model=model): + return litellm.GithubCopilotResponsesAPIConfig() + return None elif litellm.LlmProviders.CHATGPT == provider: return litellm.ChatGPTResponsesAPIConfig() elif litellm.LlmProviders.LITELLM_PROXY == provider: diff --git a/model_prices_and_context_window.json b/model_prices_and_context_window.json index bbe13442d63..d8f285335fa 100644 --- a/model_prices_and_context_window.json +++ b/model_prices_and_context_window.json @@ -17142,297 +17142,315 @@ }, "github_copilot/claude-haiku-4.5": { "litellm_provider": "github_copilot", - "max_input_tokens": 128000, - "max_output_tokens": 16000, - "max_tokens": 16000, + "max_input_tokens": 136000, + "max_output_tokens": 64000, + "max_tokens": 64000, "mode": "chat", "supported_endpoints": [ "/v1/chat/completions" ], "supports_function_calling": true, + "supports_tool_choice": true, "supports_parallel_function_calling": true, - "supports_vision": true + "supports_vision": true, + "supports_reasoning": true }, "github_copilot/claude-opus-4.5": { "litellm_provider": "github_copilot", - "max_input_tokens": 128000, - "max_output_tokens": 16000, - "max_tokens": 16000, + "max_input_tokens": 168000, + "max_output_tokens": 32000, + "max_tokens": 32000, "mode": "chat", "supported_endpoints": [ "/v1/chat/completions" ], "supports_function_calling": true, + "supports_tool_choice": true, "supports_parallel_function_calling": true, - "supports_vision": true + "supports_vision": true, + "supports_reasoning": true }, - "github_copilot/claude-opus-4.6-fast": { + "github_copilot/claude-opus-4.6": { "litellm_provider": "github_copilot", - "max_input_tokens": 128000, - "max_output_tokens": 16000, - "max_tokens": 16000, + "max_input_tokens": 168000, + "max_output_tokens": 32000, + "max_tokens": 32000, "mode": "chat", "supported_endpoints": [ "/v1/chat/completions" ], "supports_function_calling": true, + "supports_tool_choice": true, "supports_parallel_function_calling": true, - "supports_vision": true + "supports_response_schema": true, + "supports_vision": true, + "supports_reasoning": true }, - "github_copilot/claude-opus-41": { + "github_copilot/claude-opus-4.7": { "litellm_provider": "github_copilot", - "max_input_tokens": 80000, - "max_output_tokens": 16000, - "max_tokens": 16000, + "max_input_tokens": 168000, + "max_output_tokens": 32000, + "max_tokens": 32000, "mode": "chat", "supported_endpoints": [ "/v1/chat/completions" ], - "supports_vision": true + "supports_function_calling": true, + "supports_tool_choice": true, + "supports_parallel_function_calling": true, + "supports_response_schema": true, + "supports_vision": true, + "supports_reasoning": true }, - "github_copilot/claude-sonnet-4": { + "github_copilot/claude-opus-4.7-high": { "litellm_provider": "github_copilot", - "max_input_tokens": 128000, - "max_output_tokens": 16000, - "max_tokens": 16000, + "max_input_tokens": 168000, + "max_output_tokens": 32000, + "max_tokens": 32000, "mode": "chat", "supported_endpoints": [ "/v1/chat/completions" ], "supports_function_calling": true, + "supports_tool_choice": true, "supports_parallel_function_calling": true, - "supports_vision": true + "supports_response_schema": true, + "supports_vision": true, + "supports_reasoning": true }, - "github_copilot/claude-sonnet-4.5": { + "github_copilot/claude-opus-4.7-xhigh": { "litellm_provider": "github_copilot", - "max_input_tokens": 128000, - "max_output_tokens": 16000, - "max_tokens": 16000, + "max_input_tokens": 168000, + "max_output_tokens": 32000, + "max_tokens": 32000, "mode": "chat", "supported_endpoints": [ "/v1/chat/completions" ], "supports_function_calling": true, + "supports_tool_choice": true, "supports_parallel_function_calling": true, - "supports_vision": true + "supports_response_schema": true, + "supports_vision": true, + "supports_reasoning": true, + "supports_xhigh_reasoning_effort": true }, - "github_copilot/gemini-2.5-pro": { + "github_copilot/claude-sonnet-4": { "litellm_provider": "github_copilot", "max_input_tokens": 128000, - "max_output_tokens": 64000, - "max_tokens": 64000, + "max_output_tokens": 16000, + "max_tokens": 16000, "mode": "chat", + "supported_endpoints": [ + "/v1/chat/completions" + ], "supports_function_calling": true, + "supports_tool_choice": true, "supports_parallel_function_calling": true, - "supports_vision": true + "supports_vision": true, + "supports_reasoning": true }, - "github_copilot/gemini-3-pro-preview": { + "github_copilot/claude-sonnet-4.5": { "litellm_provider": "github_copilot", - "max_input_tokens": 128000, - "max_output_tokens": 64000, - "max_tokens": 64000, + "max_input_tokens": 168000, + "max_output_tokens": 32000, + "max_tokens": 32000, "mode": "chat", + "supported_endpoints": [ + "/v1/chat/completions" + ], "supports_function_calling": true, + "supports_tool_choice": true, "supports_parallel_function_calling": true, - "supports_vision": true - }, - "github_copilot/gpt-3.5-turbo": { - "litellm_provider": "github_copilot", - "max_input_tokens": 16384, - "max_output_tokens": 4096, - "max_tokens": 4096, - "mode": "chat", - "supports_function_calling": true - }, - "github_copilot/gpt-3.5-turbo-0613": { - "litellm_provider": "github_copilot", - "max_input_tokens": 16384, - "max_output_tokens": 4096, - "max_tokens": 4096, - "mode": "chat", - "supports_function_calling": true - }, - "github_copilot/gpt-4": { - "litellm_provider": "github_copilot", - "max_input_tokens": 32768, - "max_output_tokens": 4096, - "max_tokens": 4096, - "mode": "chat", - "supports_function_calling": true - }, - "github_copilot/gpt-4-0613": { - "litellm_provider": "github_copilot", - "max_input_tokens": 32768, - "max_output_tokens": 4096, - "max_tokens": 4096, - "mode": "chat", - "supports_function_calling": true - }, - "github_copilot/gpt-4-o-preview": { - "litellm_provider": "github_copilot", - "max_input_tokens": 64000, - "max_output_tokens": 4096, - "max_tokens": 4096, - "mode": "chat", - "supports_function_calling": true, - "supports_parallel_function_calling": true + "supports_vision": true, + "supports_reasoning": true }, - "github_copilot/gpt-4.1": { + "github_copilot/claude-sonnet-4.6": { "litellm_provider": "github_copilot", - "max_input_tokens": 128000, - "max_output_tokens": 16384, - "max_tokens": 16384, + "max_input_tokens": 168000, + "max_output_tokens": 32000, + "max_tokens": 32000, "mode": "chat", + "supported_endpoints": [ + "/v1/chat/completions" + ], "supports_function_calling": true, + "supports_tool_choice": true, "supports_parallel_function_calling": true, "supports_response_schema": true, - "supports_vision": true + "supports_vision": true, + "supports_reasoning": true }, - "github_copilot/gpt-4.1-2025-04-14": { + "github_copilot/gemini-2.5-pro": { "litellm_provider": "github_copilot", "max_input_tokens": 128000, - "max_output_tokens": 16384, - "max_tokens": 16384, + "max_output_tokens": 64000, + "max_tokens": 64000, "mode": "chat", "supports_function_calling": true, + "supports_tool_choice": true, "supports_parallel_function_calling": true, - "supports_response_schema": true, - "supports_vision": true - }, - "github_copilot/gpt-41-copilot": { - "litellm_provider": "github_copilot", - "mode": "completion" + "supports_vision": true, + "supports_reasoning": true }, - "github_copilot/gpt-4o": { + "github_copilot/gemini-3-flash-preview": { "litellm_provider": "github_copilot", - "max_input_tokens": 64000, - "max_output_tokens": 4096, - "max_tokens": 4096, + "max_input_tokens": 128000, + "max_output_tokens": 64000, + "max_tokens": 64000, "mode": "chat", "supports_function_calling": true, + "supports_tool_choice": true, "supports_parallel_function_calling": true, - "supports_vision": true + "supports_vision": true, + "supports_reasoning": true }, - "github_copilot/gpt-4o-2024-05-13": { + "github_copilot/gemini-3.1-pro-preview": { "litellm_provider": "github_copilot", - "max_input_tokens": 64000, - "max_output_tokens": 4096, - "max_tokens": 4096, + "max_input_tokens": 136000, + "max_output_tokens": 64000, + "max_tokens": 64000, "mode": "chat", + "supported_endpoints": [ + "/v1/chat/completions" + ], "supports_function_calling": true, + "supports_tool_choice": true, "supports_parallel_function_calling": true, - "supports_vision": true - }, - "github_copilot/gpt-4o-2024-08-06": { - "litellm_provider": "github_copilot", - "max_input_tokens": 64000, - "max_output_tokens": 16384, - "max_tokens": 16384, - "mode": "chat", - "supports_function_calling": true, - "supports_parallel_function_calling": true + "supports_vision": true, + "supports_reasoning": true }, - "github_copilot/gpt-4o-2024-11-20": { + "github_copilot/gpt-4.1": { "litellm_provider": "github_copilot", "max_input_tokens": 64000, "max_output_tokens": 16384, "max_tokens": 16384, "mode": "chat", "supports_function_calling": true, + "supports_tool_choice": true, "supports_parallel_function_calling": true, + "supports_response_schema": true, "supports_vision": true }, - "github_copilot/gpt-4o-mini": { + "github_copilot/gpt-4o": { "litellm_provider": "github_copilot", "max_input_tokens": 64000, "max_output_tokens": 4096, "max_tokens": 4096, "mode": "chat", "supports_function_calling": true, - "supports_parallel_function_calling": true + "supports_tool_choice": true, + "supports_parallel_function_calling": true, + "supports_vision": true }, - "github_copilot/gpt-4o-mini-2024-07-18": { + "github_copilot/gpt-5-mini": { "litellm_provider": "github_copilot", - "max_input_tokens": 64000, - "max_output_tokens": 4096, - "max_tokens": 4096, - "mode": "chat", + "max_input_tokens": 128000, + "max_output_tokens": 64000, + "max_tokens": 64000, + "mode": "responses", + "supported_endpoints": [ + "/v1/chat/completions", + "/v1/responses" + ], "supports_function_calling": true, - "supports_parallel_function_calling": true + "supports_tool_choice": true, + "supports_parallel_function_calling": true, + "supports_response_schema": true, + "supports_vision": true, + "supports_reasoning": true }, - "github_copilot/gpt-5": { + "github_copilot/gpt-5.2": { "litellm_provider": "github_copilot", - "max_input_tokens": 128000, + "max_input_tokens": 272000, "max_output_tokens": 128000, "max_tokens": 128000, - "mode": "chat", + "mode": "responses", "supported_endpoints": [ "/v1/chat/completions", "/v1/responses" ], "supports_function_calling": true, + "supports_tool_choice": true, "supports_parallel_function_calling": true, "supports_response_schema": true, - "supports_vision": true + "supports_vision": true, + "supports_reasoning": true, + "supports_xhigh_reasoning_effort": true }, - "github_copilot/gpt-5-mini": { + "github_copilot/gpt-5.2-codex": { "litellm_provider": "github_copilot", - "max_input_tokens": 128000, - "max_output_tokens": 64000, - "max_tokens": 64000, - "mode": "chat", + "max_input_tokens": 272000, + "max_output_tokens": 128000, + "max_tokens": 128000, + "mode": "responses", + "supported_endpoints": [ + "/v1/responses" + ], "supports_function_calling": true, + "supports_tool_choice": true, "supports_parallel_function_calling": true, "supports_response_schema": true, - "supports_vision": true + "supports_vision": true, + "supports_reasoning": true, + "supports_xhigh_reasoning_effort": true }, - "github_copilot/gpt-5.1": { + "github_copilot/gpt-5.3-codex": { "litellm_provider": "github_copilot", - "max_input_tokens": 128000, - "max_output_tokens": 64000, - "max_tokens": 64000, - "mode": "chat", + "max_input_tokens": 272000, + "max_output_tokens": 128000, + "max_tokens": 128000, + "mode": "responses", "supported_endpoints": [ - "/v1/chat/completions", "/v1/responses" ], "supports_function_calling": true, + "supports_tool_choice": true, "supports_parallel_function_calling": true, "supports_response_schema": true, - "supports_vision": true + "supports_vision": true, + "supports_reasoning": true, + "supports_xhigh_reasoning_effort": true }, - "github_copilot/gpt-5.1-codex-max": { + "github_copilot/gpt-5.4": { "litellm_provider": "github_copilot", - "max_input_tokens": 128000, + "max_input_tokens": 272000, "max_output_tokens": 128000, "max_tokens": 128000, "mode": "responses", "supported_endpoints": [ - "/v1/responses" + "/v1/responses", + "/v1/chat/completions" ], "supports_function_calling": true, + "supports_tool_choice": true, "supports_parallel_function_calling": true, "supports_response_schema": true, - "supports_vision": true + "supports_vision": true, + "supports_reasoning": true, + "supports_xhigh_reasoning_effort": true }, - "github_copilot/gpt-5.2": { + "github_copilot/gpt-5.4-mini": { "litellm_provider": "github_copilot", - "max_input_tokens": 128000, - "max_output_tokens": 64000, - "max_tokens": 64000, - "mode": "chat", + "max_input_tokens": 272000, + "max_output_tokens": 128000, + "max_tokens": 128000, + "mode": "responses", "supported_endpoints": [ - "/v1/chat/completions", "/v1/responses" ], "supports_function_calling": true, + "supports_tool_choice": true, "supports_parallel_function_calling": true, "supports_response_schema": true, - "supports_vision": true + "supports_vision": true, + "supports_reasoning": true, + "supports_none_reasoning_effort": true, + "supports_xhigh_reasoning_effort": true }, - "github_copilot/gpt-5.3-codex": { + "github_copilot/gpt-5.5": { "litellm_provider": "github_copilot", - "max_input_tokens": 128000, + "max_input_tokens": 272000, "max_output_tokens": 128000, "max_tokens": 128000, "mode": "responses", @@ -17440,9 +17458,13 @@ "/v1/responses" ], "supports_function_calling": true, + "supports_tool_choice": true, "supports_parallel_function_calling": true, "supports_response_schema": true, - "supports_vision": true + "supports_vision": true, + "supports_reasoning": true, + "supports_none_reasoning_effort": true, + "supports_xhigh_reasoning_effort": true }, "github_copilot/text-embedding-3-small": { "litellm_provider": "github_copilot", diff --git a/tests/test_litellm/llms/github_copilot/responses/test_github_copilot_responses_transformation.py b/tests/test_litellm/llms/github_copilot/responses/test_github_copilot_responses_transformation.py index 54e7170bb20..f8d7ab9425f 100644 --- a/tests/test_litellm/llms/github_copilot/responses/test_github_copilot_responses_transformation.py +++ b/tests/test_litellm/llms/github_copilot/responses/test_github_copilot_responses_transformation.py @@ -14,6 +14,8 @@ sys.path.insert(0, os.path.abspath("../../../../..")) import pytest +import litellm +from litellm.litellm_core_utils.get_model_cost_map import get_model_cost_map from litellm.types.utils import LlmProviders from litellm.utils import ProviderConfigManager from litellm.llms.github_copilot.responses.transformation import ( @@ -22,13 +24,26 @@ from litellm.types.llms.openai import ResponsesAPIOptionalRequestParams +@pytest.fixture(autouse=True) +def use_local_model_cost_map(monkeypatch: pytest.MonkeyPatch): + """Pin litellm.model_cost to the bundled local backup so tests don't depend + on remote catalog fetches (and don't change behavior across remote refreshes).""" + monkeypatch.setenv("LITELLM_LOCAL_MODEL_COST_MAP", "True") + monkeypatch.setattr( + litellm, "model_cost", get_model_cost_map(url=litellm.model_cost_map_url) + ) + litellm.add_known_models(model_cost_map=litellm.model_cost) + + class TestGithubCopilotResponsesAPITransformation: """Test GitHub Copilot Responses API configuration and transformations""" def test_github_copilot_provider_config_registration(self): - """Test that GitHub Copilot provider returns GithubCopilotResponsesAPIConfig""" + """Test that GitHub Copilot provider returns the native Responses API + config for a Responses-capable catalog model. Exercises the full stack: + catalog lookup -> github_copilot_supports_responses_api -> native config.""" config = ProviderConfigManager.get_provider_responses_api_config( - model="github_copilot/gpt-5.1-codex", + model="github_copilot/gpt-5.3-codex", provider=LlmProviders.GITHUB_COPILOT, ) @@ -373,3 +388,174 @@ def test_handle_reasoning_item_non_reasoning_passthrough(self): # Non-reasoning items should pass through unchanged assert result == message_item + + +class TestGithubCopilotResponsesAPIRouting: + """``ProviderConfigManager.get_provider_responses_api_config`` for github_copilot + returns the native Responses config only when the model has ``mode=responses`` + in the (already-merged) model info; otherwise returns None so the dispatcher + routes through the chat-completions translation bridge.""" + + @patch( + "litellm.llms.github_copilot.responses.transformation._get_model_info_helper" + ) + def test_returns_config_when_mode_is_responses(self, mock_get_info): + """``mode=responses`` returns native config.""" + mock_get_info.return_value = {"mode": "responses"} + config = ProviderConfigManager.get_provider_responses_api_config( + model="github_copilot/some-responses-model", + provider=LlmProviders.GITHUB_COPILOT, + ) + assert isinstance(config, GithubCopilotResponsesAPIConfig) + + @patch( + "litellm.llms.github_copilot.responses.transformation._get_model_info_helper" + ) + def test_returns_none_when_mode_is_chat(self, mock_get_info): + """``mode=chat`` returns None so dispatcher uses bridge.""" + mock_get_info.return_value = {"mode": "chat"} + config = ProviderConfigManager.get_provider_responses_api_config( + model="github_copilot/some-chat-only-model", + provider=LlmProviders.GITHUB_COPILOT, + ) + assert config is None + + @patch( + "litellm.llms.github_copilot.responses.transformation._get_model_info_helper" + ) + def test_returns_none_when_mode_is_unset_and_no_endpoints(self, mock_get_info): + """Entry without ``mode`` and without ``supported_endpoints`` returns None + (conservative default).""" + mock_get_info.return_value = {} + config = ProviderConfigManager.get_provider_responses_api_config( + model="github_copilot/some-model", + provider=LlmProviders.GITHUB_COPILOT, + ) + assert config is None + + @patch( + "litellm.llms.github_copilot.responses.transformation._get_model_info_helper" + ) + def test_returns_config_when_mode_unset_but_endpoints_have_responses( + self, mock_get_info + ): + """``mode`` unset but ``supported_endpoints`` declaring /v1/responses + returns native config (endpoint-list fallback for stale-but-correct + catalog entries that lack ``mode``).""" + mock_get_info.return_value = { + "supported_endpoints": ["/v1/chat/completions", "/v1/responses"] + } + config = ProviderConfigManager.get_provider_responses_api_config( + model="github_copilot/some-model", + provider=LlmProviders.GITHUB_COPILOT, + ) + assert isinstance(config, GithubCopilotResponsesAPIConfig) + + @patch( + "litellm.llms.github_copilot.responses.transformation._get_model_info_helper" + ) + def test_mode_chat_overrides_endpoints_with_responses(self, mock_get_info): + """``mode=chat`` is a hard opt-out: forces bridge even when + ``supported_endpoints`` includes /v1/responses. Lets users force the + bridge for dual-endpoint models without clearing endpoint metadata.""" + mock_get_info.return_value = { + "mode": "chat", + "supported_endpoints": ["/v1/chat/completions", "/v1/responses"], + } + config = ProviderConfigManager.get_provider_responses_api_config( + model="github_copilot/some-model", + provider=LlmProviders.GITHUB_COPILOT, + ) + assert config is None + + def test_returns_config_when_model_is_none(self): + """Follow-up GET/DELETE operations pass model=None and keep the native + config path (no per-model lookup is possible).""" + config = ProviderConfigManager.get_provider_responses_api_config( + model=None, + provider=LlmProviders.GITHUB_COPILOT, + ) + assert isinstance(config, GithubCopilotResponsesAPIConfig) + + @patch( + "litellm.llms.github_copilot.responses.transformation._get_model_info_helper" + ) + def test_returns_none_when_get_model_info_raises(self, mock_get_info): + """Catalog lookup failure (model not registered) returns None + (conservative default; bridge handles unknown models safely).""" + mock_get_info.side_effect = Exception("model not in catalog") + config = ProviderConfigManager.get_provider_responses_api_config( + model="github_copilot/never-seen-model", + provider=LlmProviders.GITHUB_COPILOT, + ) + assert config is None + + @patch( + "litellm.llms.github_copilot.responses.transformation._get_model_info_helper" + ) + def test_user_override_via_register_model(self, mock_get_info): + """User-supplied per-deployment ``model_info`` flows through + ``litellm.register_model`` (called by the router) into the merged + catalog read by ``_get_model_info_helper``. Setting ``mode=responses`` + for a model whose catalog entry says ``mode=chat`` therefore opts in + to native dispatch without any per-call argument plumbing.""" + mock_get_info.return_value = {"mode": "responses"} + config = ProviderConfigManager.get_provider_responses_api_config( + model="github_copilot/some-chat-only-model", + provider=LlmProviders.GITHUB_COPILOT, + ) + assert isinstance(config, GithubCopilotResponsesAPIConfig) + + @patch( + "litellm.llms.github_copilot.responses.transformation._get_model_info_helper" + ) + def test_realistic_chat_only_entry_returns_none(self, mock_get_info): + """Realistic ``model_prices_and_context_window.json`` shape for a + chat-only Copilot model (e.g. github_copilot/gemini-3.1-pro-preview) + returns None so /v1/responses calls fall back to the bridge.""" + mock_get_info.return_value = { + "litellm_provider": "github_copilot", + "max_input_tokens": 136000, + "max_output_tokens": 64000, + "max_tokens": 64000, + "mode": "chat", + "supported_endpoints": ["/v1/chat/completions"], + "supports_function_calling": True, + "supports_tool_choice": True, + "supports_parallel_function_calling": True, + "supports_vision": True, + "supports_reasoning": True, + } + config = ProviderConfigManager.get_provider_responses_api_config( + model="github_copilot/some-chat-only-model", + provider=LlmProviders.GITHUB_COPILOT, + ) + assert config is None + + @patch( + "litellm.llms.github_copilot.responses.transformation._get_model_info_helper" + ) + def test_realistic_responses_only_entry_returns_config(self, mock_get_info): + """Realistic catalog entry for a Responses-only Copilot model + (e.g. github_copilot/gpt-5.5) returns the native config.""" + mock_get_info.return_value = { + "litellm_provider": "github_copilot", + "max_input_tokens": 272000, + "max_output_tokens": 128000, + "max_tokens": 128000, + "mode": "responses", + "supported_endpoints": ["/v1/responses"], + "supports_function_calling": True, + "supports_tool_choice": True, + "supports_parallel_function_calling": True, + "supports_response_schema": True, + "supports_vision": True, + "supports_reasoning": True, + "supports_none_reasoning_effort": True, + "supports_xhigh_reasoning_effort": True, + } + config = ProviderConfigManager.get_provider_responses_api_config( + model="github_copilot/some-responses-only-model", + provider=LlmProviders.GITHUB_COPILOT, + ) + assert isinstance(config, GithubCopilotResponsesAPIConfig)