From 190a3e01ecd96382520b24768e04f34ca18244ae Mon Sep 17 00:00:00 2001 From: zhanghanduo Date: Sun, 16 Aug 2026 18:01:27 +0800 Subject: [PATCH] fix(apodex): harden provider routing metadata --- litellm/llms/apodex/chat/transformation.py | 35 ++++++++++++++++++- .../provider_endpoints_support_backup.json | 17 +++++++++ .../apodex/test_apodex_chat_transformation.py | 11 ++++++ .../llms/apodex/test_apodex_common_utils.py | 6 ++++ .../test_apodex_messages_transformation.py | 35 ++++++++++++++++--- 5 files changed, 98 insertions(+), 6 deletions(-) diff --git a/litellm/llms/apodex/chat/transformation.py b/litellm/llms/apodex/chat/transformation.py index 4db9bc9fac1..a1b13633f66 100644 --- a/litellm/llms/apodex/chat/transformation.py +++ b/litellm/llms/apodex/chat/transformation.py @@ -15,6 +15,7 @@ from collections.abc import Mapping from typing import Final from litellm.llms.openai.chat.gpt_transformation import OpenAIGPTConfig +from litellm.types.llms.openai import AllMessageValues from ..common_utils import ( APODEX_API_BASE_URL, @@ -46,6 +47,8 @@ _CORE_PARAMS: Final = ( "parallel_tool_calls", ) +_PIN_NON_STREAMING: Final = "_apodex_pin_non_streaming" + class ApodexChatConfig(OpenAIGPTConfig): """ @@ -116,5 +119,35 @@ class ApodexChatConfig(OpenAIGPTConfig): ) return { # mutable-ok: JSON request body **renamed, - "extra_body": {"stream": False, **extra_body}, # mutable-ok: JSON request body + _PIN_NON_STREAMING: True, + "extra_body": {**extra_body, "stream": False}, # mutable-ok: JSON request body + } + + def transform_request( + self, + model: str, + messages: list[AllMessageValues], + optional_params: dict, # mutable-ok: matches the base-class signature + litellm_params: dict, # mutable-ok: matches the base-class signature + headers: dict, # mutable-ok: matches the base-class signature + ) -> dict: # mutable-ok: JSON request body + """Apply the non-streaming pin after LiteLLM merges caller ``extra_body``.""" + pin_non_streaming: Final = bool(optional_params.pop(_PIN_NON_STREAMING, False)) + transformed: Final = super().transform_request( + model=model, + messages=messages, + optional_params=optional_params, + litellm_params=litellm_params, + headers=headers, + ) + if not pin_non_streaming: + return transformed + + requested_extra_body: Final = transformed.get("extra_body") + extra_body: Final = ( + requested_extra_body if isinstance(requested_extra_body, Mapping) else {} # mutable-ok: JSON request body + ) + return { # mutable-ok: JSON request body + **transformed, + "extra_body": {**extra_body, "stream": False}, # mutable-ok: JSON request body } diff --git a/litellm/provider_endpoints_support_backup.json b/litellm/provider_endpoints_support_backup.json index dd7712aabca..f15dcb2dfb2 100644 --- a/litellm/provider_endpoints_support_backup.json +++ b/litellm/provider_endpoints_support_backup.json @@ -177,6 +177,23 @@ "interactions": true } }, + "apodex": { + "display_name": "Apodex (`apodex`)", + "url": "https://docs.litellm.ai/docs/providers/apodex", + "endpoints": { + "chat_completions": true, + "messages": true, + "responses": true, + "embeddings": false, + "image_generations": false, + "audio_transcriptions": false, + "audio_speech": false, + "moderations": false, + "batches": false, + "rerank": false, + "a2a": false + } + }, "apertis": { "display_name": "Apertis (`apertis`)", "endpoints": { diff --git a/tests/test_litellm/llms/apodex/test_apodex_chat_transformation.py b/tests/test_litellm/llms/apodex/test_apodex_chat_transformation.py index da31464d5cb..2655a14aae7 100644 --- a/tests/test_litellm/llms/apodex/test_apodex_chat_transformation.py +++ b/tests/test_litellm/llms/apodex/test_apodex_chat_transformation.py @@ -156,6 +156,17 @@ class TestStreamDefault: assert captured["body"]["stream"] is False assert captured["body"]["mcp_servers"] == mcp_servers + def test_extra_body_cannot_override_non_streaming_pin(self): + captured: dict = {} + litellm.completion( + model=DEEP_RESEARCH_MODEL, + messages=[{"role": "user", "content": "hi"}], + extra_body={"stream": True}, + client=_client(captured), + ) + + assert captured["body"]["stream"] is False + class TestSupportedParams: def test_core_models_support_tools(self): diff --git a/tests/test_litellm/llms/apodex/test_apodex_common_utils.py b/tests/test_litellm/llms/apodex/test_apodex_common_utils.py index 2da109c6ec4..da908d4d8e8 100644 --- a/tests/test_litellm/llms/apodex/test_apodex_common_utils.py +++ b/tests/test_litellm/llms/apodex/test_apodex_common_utils.py @@ -82,6 +82,12 @@ class TestRegistration: assert litellm.ApodexChatConfig().custom_llm_provider == "apodex" assert litellm.ApodexResponsesConfig().custom_llm_provider == LlmProviders.APODEX + def test_packaged_endpoint_matrix_matches_the_source(self): + source = json.loads((REPO_ROOT / "provider_endpoints_support.json").read_text()) + backup = json.loads((REPO_ROOT / "litellm" / "provider_endpoints_support_backup.json").read_text()) + + assert backup["providers"]["apodex"] == source["providers"]["apodex"] + class TestModelMetadata: @pytest.fixture(scope="class") diff --git a/tests/test_litellm/llms/apodex/test_apodex_messages_transformation.py b/tests/test_litellm/llms/apodex/test_apodex_messages_transformation.py index 09427b31a2f..0e4fb368cc5 100644 --- a/tests/test_litellm/llms/apodex/test_apodex_messages_transformation.py +++ b/tests/test_litellm/llms/apodex/test_apodex_messages_transformation.py @@ -62,12 +62,37 @@ class TestNativePassthroughRouting: forwarding to a path Apodex does not serve for these tiers.""" assert _messages_config(model) is None - def test_deep_research_translation_uses_responses_api(self): - from litellm.llms.anthropic.experimental_pass_through.messages.handler import ( - _should_route_to_responses_api, - ) + @pytest.mark.parametrize("stream", (False, True), ids=("non-streaming", "streaming")) + def test_deep_research_translation_uses_responses_api(self, monkeypatch: pytest.MonkeyPatch, stream: bool): + from litellm.llms.anthropic.experimental_pass_through.messages import handler - assert _should_route_to_responses_api("apodex") is True + captured: dict = {} + + class ResponsesRouteSelected(Exception): + pass + + def capture_responses_translation(**kwargs): + captured.update(kwargs) + raise ResponsesRouteSelected + + def reject_chat_translation(**kwargs): + pytest.fail("Apodex Deep Research messages must not route through chat completions") + + monkeypatch.setattr(litellm, "responses", capture_responses_translation) + monkeypatch.setattr(litellm, "completion", reject_chat_translation) + + with pytest.raises(ResponsesRouteSelected): + handler.anthropic_messages_handler( + max_tokens=256, + messages=[{"role": "user", "content": "hi"}], + model="apodex/apodex-1-1-deep-research", + custom_llm_provider="apodex", + stream=stream, + ) + + assert captured["model"] == "apodex-1-1-deep-research" + assert captured["custom_llm_provider"] == "apodex" + assert captured.get("stream", False) is stream class TestNativePassthroughRequest: