fix(apodex): isolate chat routing

This commit is contained in:
zhanghanduo 2026-08-16 19:47:24 +08:00
parent 190a3e01ec
commit 42dcfa12a2
3 changed files with 42 additions and 13 deletions

View file

@ -14,6 +14,7 @@ Ref: https://platform.apodex.ai/docs/chat-completions
from collections.abc import Mapping
from typing import Final
import litellm
from litellm.llms.openai.chat.gpt_transformation import OpenAIGPTConfig
from litellm.types.llms.openai import AllMessageValues
@ -22,6 +23,7 @@ from ..common_utils import (
get_apodex_api_base,
get_apodex_api_key,
is_deep_research_model,
is_responses_only_model,
)
_DEEP_RESEARCH_PARAMS: Final = (
@ -87,6 +89,13 @@ class ApodexChatConfig(OpenAIGPTConfig):
model: str,
drop_params: bool,
) -> dict: # mutable-ok: matches the base-class signature
if is_responses_only_model(model):
raise litellm.BadRequestError(
message=f"apodex model {model} is only available through /v1/responses",
model=model,
llm_provider="apodex",
)
mapped: Final = super().map_openai_params(
non_default_params=non_default_params,
optional_params=optional_params,
@ -94,7 +103,6 @@ class ApodexChatConfig(OpenAIGPTConfig):
drop_params=drop_params,
)
# Apodex documents max_tokens only.
renamed: Final = (
mapped
if "max_completion_tokens" not in mapped
@ -109,18 +117,9 @@ class ApodexChatConfig(OpenAIGPTConfig):
if renamed.get("stream"):
return renamed
# The OpenAI chat handler pops `stream` out of the params it forwards
# (litellm/llms/openai/openai.py), which would leave the Deep Research tiers
# on their streaming default. extra_body is merged into the request body
# further down, so it survives that pop.
requested_extra_body: Final = renamed.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
**renamed,
_PIN_NON_STREAMING: True,
"extra_body": {**extra_body, "stream": False}, # mutable-ok: JSON request body
}
def transform_request(
@ -131,12 +130,12 @@ class ApodexChatConfig(OpenAIGPTConfig):
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))
pin_non_streaming: Final = bool(optional_params.get(_PIN_NON_STREAMING, False))
forwarded_params: Final = {key: value for key, value in optional_params.items() if key != _PIN_NON_STREAMING}
transformed: Final = super().transform_request(
model=model,
messages=messages,
optional_params=optional_params,
optional_params=forwarded_params,
litellm_params=litellm_params,
headers=headers,
)

View file

@ -17,6 +17,7 @@ from litellm.secret_managers.main import get_secret_str
APODEX_API_BASE_URL: Final = "https://api.apodex.ai/v1"
_DEEP_RESEARCH_MARKER: Final = "-deep-"
_RESPONSES_ONLY_MODELS: Final = frozenset({"apodex-1-1-deep-discover"})
def strip_provider_prefix(model: str) -> str:
@ -28,6 +29,10 @@ def is_deep_research_model(model: str) -> bool:
return _DEEP_RESEARCH_MARKER in strip_provider_prefix(model)
def is_responses_only_model(model: str) -> bool:
return strip_provider_prefix(model) in _RESPONSES_ONLY_MODELS
def get_apodex_api_key(api_key: str | None = None) -> str | None:
return api_key or get_secret_str("APODEX_API_KEY")

View file

@ -167,8 +167,33 @@ class TestStreamDefault:
assert captured["body"]["stream"] is False
def test_transform_does_not_mutate_optional_params(self):
config = _chat_config("apodex-1.1")
optional_params = config.map_openai_params(
non_default_params={}, optional_params={}, model="apodex-1.1", drop_params=False
)
original = optional_params.copy()
config.transform_request(
model="apodex-1.1",
messages=[{"role": "user", "content": "hi"}],
optional_params=optional_params,
litellm_params={"custom_llm_provider": "apodex"},
headers={},
)
assert optional_params == original
class TestSupportedParams:
def test_responses_only_model_rejects_chat_completions(self):
with pytest.raises(litellm.BadRequestError, match="only available through /v1/responses"):
litellm.completion(
model="apodex/apodex-1-1-deep-discover",
messages=[{"role": "user", "content": "hi"}],
client=_client({}),
)
def test_core_models_support_tools(self):
supported = _chat_config("apodex-1.1").get_supported_openai_params("apodex-1.1")
assert "tools" in supported