feat(providers): add Apodex as an OpenAI-compatible provider

Registers apodex via providers.json with /v1/chat/completions, /v1/responses
and native /v1/messages, plus price map entries for the two core models and
the six deep research tiers.

Apodex defaults `stream` to true on both /v1/chat/completions and /v1/responses,
so a non-streaming litellm call would get SSE back and fail to parse it. Adds a
`send_explicit_stream_false` special-handling flag that pins the field on the
wire, and rewrites the JSON provider param mapping to build its result instead
of mutating the caller's dict.
This commit is contained in:
zhanghanduo 2026-08-16 11:19:57 +08:00
parent 992a8123ac
commit 20ef10e920
14 changed files with 838 additions and 35 deletions

View file

@ -279,6 +279,7 @@ curl -X POST 'http://0.0.0.0:4000/v1/chat/completions' \
| [Anthropic (`anthropic`)](https://docs.litellm.ai/docs/providers/anthropic) | ✅ | ✅ | ✅ | | | | | | ✅ | |
| [Anthropic Text (`anthropic_text`)](https://docs.litellm.ai/docs/providers/anthropic) | ✅ | ✅ | ✅ | | | | | | ✅ | |
| [Anyscale](https://docs.litellm.ai/docs/providers/anyscale) | ✅ | ✅ | ✅ | | | | | | | |
| [Apodex (`apodex`)](https://docs.litellm.ai/docs/providers/apodex) | ✅ | ✅ | ✅ | | | | | | | |
| [AssemblyAI (`assemblyai`)](https://docs.litellm.ai/docs/pass_through/assembly_ai) | ✅ | ✅ | ✅ | | | ✅ | | | | |
| [Auto Router (`auto_router`)](https://docs.litellm.ai/docs/proxy/auto_routing) | ✅ | ✅ | ✅ | | | | | | | |
| [AWS - Bedrock (`bedrock`)](https://docs.litellm.ai/docs/providers/bedrock) | ✅ | ✅ | ✅ | ✅ | | | | | | ✅ |

View file

@ -99,7 +99,7 @@
"limit": 0
},
"reportUnknownArgumentType": {
"limit": 44776
"limit": 44774
},
"reportUnknownLambdaType": {
"limit": 113
@ -111,7 +111,7 @@
"limit": 19967
},
"reportUnknownVariableType": {
"limit": 30881
"limit": 30879
},
"reportUnnecessaryCast": {
"limit": 117

View file

@ -756,6 +756,7 @@ openai_compatible_endpoints: Final[list] = [
"https://api.libertai.io/v1",
"https://pinstripes.io/v1",
"https://api.meta.ai/v1",
"https://api.apodex.ai/v1",
]
@ -823,6 +824,7 @@ openai_compatible_providers: Final[list] = [
"pinstripes", # Pinstripes - JSON-configured provider
"darkbloom",
"meta", # Meta Model API (Muse Spark) - JSON-configured provider
"apodex", # Apodex - JSON-configured provider
]
openai_text_completion_compatible_providers: Final[list] = [ # providers that support `/v1/completions`
"together_ai",

View file

@ -349,6 +349,9 @@ def get_llm_provider(
elif endpoint == "https://api.meta.ai/v1":
custom_llm_provider = "meta"
dynamic_api_key = get_secret_str("META_API_KEY")
elif endpoint == "https://api.apodex.ai/v1":
custom_llm_provider = "apodex"
dynamic_api_key = get_secret_str("APODEX_API_KEY")
if api_base is not None and not isinstance(api_base, str):
raise Exception(f"api base needs to be a string. api_base={api_base}")

View file

@ -59,7 +59,15 @@ That's it! The provider will be automatically loaded and available.
// Optional: Special handling flags
"special_handling": {
"convert_content_list_to_string": true
"convert_content_list_to_string": true,
// Send "stream": false explicitly instead of omitting it. Needed by
// providers whose /v1/chat/completions and /v1/responses default to
// streaming, where omitting the field returns SSE to a non-streaming call
"send_explicit_stream_false": true,
// Always send "store": false on /v1/responses
"force_store_false": true
}
}
}

View file

@ -2,7 +2,7 @@
Dynamic configuration class generator for JSON-based providers.
"""
from collections.abc import Coroutine
from collections.abc import Coroutine, Mapping
from typing import Any, Final, Literal, overload
from litellm._logging import verbose_logger
@ -17,6 +17,17 @@ from litellm.types.llms.openai import AllMessageValues
from .json_loader import SimpleProviderConfig
def _clamp_temperature(temperature: float, n: int, constraints: Mapping[str, float]) -> float:
capped: Final = (
min(temperature, constraints["temperature_max"]) if "temperature_max" in constraints else temperature
)
floored: Final = max(capped, constraints["temperature_min"]) if "temperature_min" in constraints else capped
floor_for_multiple_choices: Final = constraints.get("temperature_min_with_n_gt_1")
if n > 1 and floor_for_multiple_choices is not None:
return max(floored, floor_for_multiple_choices)
return floored
def create_config_class(provider: SimpleProviderConfig):
"""Generate config class dynamically from JSON configuration"""
@ -131,37 +142,36 @@ def create_config_class(provider: SimpleProviderConfig):
"""Apply parameter mappings and constraints"""
supported_params: Final = self.get_supported_openai_params(model)
mapped: Final = {
**optional_params,
**{
provider.param_mappings.get(param, param): value
for param, value in non_default_params.items()
if param in provider.param_mappings or param in supported_params
},
}
# Apply supported params
for param, value in non_default_params.items():
# Check parameter mappings first
if param in provider.param_mappings:
optional_params[provider.param_mappings[param]] = value
elif param in supported_params:
optional_params[param] = value
constrained: Final = (
mapped
if "temperature" not in mapped
else {
**mapped,
"temperature": _clamp_temperature(
temperature=mapped["temperature"],
n=mapped.get("n", 1),
constraints=provider.constraints,
),
}
)
# Apply temperature constraints if present
if "temperature" in optional_params:
temp = optional_params["temperature"]
constraints: Final = provider.constraints
# Clamp to max
if "temperature_max" in constraints:
temp = min(temp, constraints["temperature_max"])
# Clamp to min
if "temperature_min" in constraints:
temp = max(temp, constraints["temperature_min"])
# Special case: temperature_min_with_n_gt_1
if "temperature_min_with_n_gt_1" in constraints:
n: Final = optional_params.get("n", 1)
if n > 1 and temp < constraints["temperature_min_with_n_gt_1"]:
temp = constraints["temperature_min_with_n_gt_1"]
optional_params["temperature"] = temp
return optional_params
# The OpenAI SDK omits `stream` entirely when it is false, which makes
# stream-by-default providers answer a non-streaming call with SSE. Pin it
# on the wire through extra_body, which the SDK merges into the request body.
if not provider.special_handling.get("send_explicit_stream_false") or constrained.get("stream"):
return constrained
requested_extra_body: Final = constrained.get("extra_body")
extra_body: Final[dict] = requested_extra_body if isinstance(requested_extra_body, dict) else {}
return {**constrained, "extra_body": {"stream": False, **extra_body}}
@property
def custom_llm_provider(self) -> str | None:
@ -232,6 +242,8 @@ def create_responses_config_class(provider: SimpleProviderConfig):
) -> dict:
if provider.special_handling.get("force_store_false"):
response_api_optional_request_params["store"] = False
if provider.special_handling.get("send_explicit_stream_false"):
response_api_optional_request_params.setdefault("stream", False)
return super().transform_responses_api_request(
model=model,
input=input,

View file

@ -175,6 +175,18 @@
"base_class": "openai_gpt",
"supported_endpoints": ["/v1/chat/completions", "/v1/responses", "/v1/messages"]
},
"apodex": {
"base_url": "https://api.apodex.ai/v1",
"api_key_env": "APODEX_API_KEY",
"api_base_env": "APODEX_API_BASE",
"param_mappings": {
"max_completion_tokens": "max_tokens"
},
"supported_endpoints": ["/v1/chat/completions", "/v1/responses", "/v1/messages"],
"special_handling": {
"send_explicit_stream_false": true
}
},
"pinstripes": {
"base_url": "https://pinstripes.io/v1",
"api_key_env": "PINSTRIPES_API_KEY",

View file

@ -48296,6 +48296,196 @@
],
"supports_audio_output": true
},
"apodex/apodex-1.1": {
"max_tokens": 262144,
"max_input_tokens": 262144,
"max_output_tokens": 262144,
"input_cost_per_token": 3e-07,
"cache_read_input_token_cost": 3e-08,
"output_cost_per_token": 3e-06,
"input_cost_per_token_above_200k_tokens": 6e-07,
"cache_read_input_token_cost_above_200k_tokens": 6e-08,
"output_cost_per_token_above_200k_tokens": 6e-06,
"litellm_provider": "apodex",
"mode": "chat",
"source": "https://platform.apodex.ai/docs/models",
"supported_endpoints": [
"/v1/chat/completions",
"/v1/responses",
"/v1/messages"
],
"supports_function_calling": true,
"supports_native_streaming": true,
"supports_prompt_caching": true,
"supports_reasoning": true,
"supports_system_messages": true,
"supports_tool_choice": true,
"supports_vision": false
},
"apodex/apodex-1.1-mini": {
"max_tokens": 262144,
"max_input_tokens": 262144,
"max_output_tokens": 262144,
"input_cost_per_token": 1e-07,
"cache_read_input_token_cost": 1e-08,
"output_cost_per_token": 1e-06,
"input_cost_per_token_above_200k_tokens": 2e-07,
"cache_read_input_token_cost_above_200k_tokens": 2e-08,
"output_cost_per_token_above_200k_tokens": 2e-06,
"litellm_provider": "apodex",
"mode": "chat",
"source": "https://platform.apodex.ai/docs/models",
"supported_endpoints": [
"/v1/chat/completions",
"/v1/responses",
"/v1/messages"
],
"supports_function_calling": true,
"supports_native_streaming": true,
"supports_prompt_caching": true,
"supports_reasoning": true,
"supports_system_messages": true,
"supports_tool_choice": true,
"supports_vision": false
},
"apodex/apodex-1-1-deep-research": {
"max_tokens": 65536,
"max_input_tokens": 131072,
"max_output_tokens": 65536,
"input_cost_per_token": 5e-06,
"output_cost_per_token": 2e-05,
"litellm_provider": "apodex",
"mode": "chat",
"source": "https://platform.apodex.ai/docs/pricing",
"supported_endpoints": [
"/v1/chat/completions",
"/v1/responses"
],
"supports_function_calling": false,
"supports_native_streaming": true,
"supports_prompt_caching": false,
"supports_reasoning": true,
"supports_response_schema": false,
"supports_system_messages": true,
"supports_tool_choice": false,
"supports_vision": false,
"supports_web_search": true
},
"apodex/apodex-1-1-deep-solve": {
"max_tokens": 65536,
"max_input_tokens": 131072,
"max_output_tokens": 65536,
"input_cost_per_token": 5e-06,
"output_cost_per_token": 2.5e-05,
"litellm_provider": "apodex",
"mode": "chat",
"source": "https://platform.apodex.ai/docs/pricing",
"supported_endpoints": [
"/v1/chat/completions",
"/v1/responses"
],
"supports_function_calling": false,
"supports_native_streaming": true,
"supports_prompt_caching": false,
"supports_reasoning": true,
"supports_response_schema": false,
"supports_system_messages": true,
"supports_tool_choice": false,
"supports_vision": false,
"supports_web_search": true
},
"apodex/apodex-1-1-deep-discover": {
"max_tokens": 262144,
"max_input_tokens": 131072,
"max_output_tokens": 262144,
"input_cost_per_token": 1e-05,
"output_cost_per_token": 0.0001,
"litellm_provider": "apodex",
"mode": "chat",
"source": "https://platform.apodex.ai/docs/pricing",
"supported_endpoints": [
"/v1/chat/completions",
"/v1/responses"
],
"supports_function_calling": false,
"supports_native_streaming": true,
"supports_prompt_caching": false,
"supports_reasoning": true,
"supports_response_schema": false,
"supports_system_messages": true,
"supports_tool_choice": false,
"supports_vision": false,
"supports_web_search": true
},
"apodex/apodex-1-0-deep-research": {
"max_tokens": 16384,
"max_input_tokens": 262144,
"max_output_tokens": 16384,
"input_cost_per_token": 1e-05,
"output_cost_per_token": 4e-05,
"litellm_provider": "apodex",
"mode": "chat",
"source": "https://platform.apodex.ai/docs/pricing",
"supported_endpoints": [
"/v1/chat/completions",
"/v1/responses"
],
"supports_function_calling": false,
"supports_native_streaming": true,
"supports_prompt_caching": false,
"supports_reasoning": true,
"supports_response_schema": false,
"supports_system_messages": true,
"supports_tool_choice": false,
"supports_vision": false,
"supports_web_search": true
},
"apodex/apodex-1-0-deep-solve": {
"max_tokens": 16384,
"max_input_tokens": 262144,
"max_output_tokens": 16384,
"input_cost_per_token": 1e-05,
"output_cost_per_token": 5e-05,
"litellm_provider": "apodex",
"mode": "chat",
"source": "https://platform.apodex.ai/docs/pricing",
"supported_endpoints": [
"/v1/chat/completions",
"/v1/responses"
],
"supports_function_calling": false,
"supports_native_streaming": true,
"supports_prompt_caching": false,
"supports_reasoning": true,
"supports_response_schema": false,
"supports_system_messages": true,
"supports_tool_choice": false,
"supports_vision": false,
"supports_web_search": true
},
"apodex/apodex-1-0-deep-discover": {
"max_tokens": 262144,
"max_input_tokens": 131072,
"max_output_tokens": 262144,
"input_cost_per_token": 1e-05,
"output_cost_per_token": 0.0001,
"litellm_provider": "apodex",
"mode": "chat",
"source": "https://platform.apodex.ai/docs/pricing",
"supported_endpoints": [
"/v1/chat/completions",
"/v1/responses"
],
"supports_function_calling": false,
"supports_native_streaming": true,
"supports_prompt_caching": false,
"supports_reasoning": true,
"supports_response_schema": false,
"supports_system_messages": true,
"supports_tool_choice": false,
"supports_vision": false,
"supports_web_search": true
},
"fallback_generalizations": {
"rules": [
{

View file

@ -3727,6 +3727,7 @@ class LlmProviders(str, Enum):
PINSTRIPES = "pinstripes"
DARKBLOOM = "darkbloom"
META = "meta"
APODEX = "apodex"
LITELLM_AGENT = "litellm_agent"
CURSOR = "cursor"
BEDROCK_MANTLE = "bedrock_mantle"

View file

@ -48296,6 +48296,196 @@
],
"supports_audio_output": true
},
"apodex/apodex-1.1": {
"max_tokens": 262144,
"max_input_tokens": 262144,
"max_output_tokens": 262144,
"input_cost_per_token": 3e-07,
"cache_read_input_token_cost": 3e-08,
"output_cost_per_token": 3e-06,
"input_cost_per_token_above_200k_tokens": 6e-07,
"cache_read_input_token_cost_above_200k_tokens": 6e-08,
"output_cost_per_token_above_200k_tokens": 6e-06,
"litellm_provider": "apodex",
"mode": "chat",
"source": "https://platform.apodex.ai/docs/models",
"supported_endpoints": [
"/v1/chat/completions",
"/v1/responses",
"/v1/messages"
],
"supports_function_calling": true,
"supports_native_streaming": true,
"supports_prompt_caching": true,
"supports_reasoning": true,
"supports_system_messages": true,
"supports_tool_choice": true,
"supports_vision": false
},
"apodex/apodex-1.1-mini": {
"max_tokens": 262144,
"max_input_tokens": 262144,
"max_output_tokens": 262144,
"input_cost_per_token": 1e-07,
"cache_read_input_token_cost": 1e-08,
"output_cost_per_token": 1e-06,
"input_cost_per_token_above_200k_tokens": 2e-07,
"cache_read_input_token_cost_above_200k_tokens": 2e-08,
"output_cost_per_token_above_200k_tokens": 2e-06,
"litellm_provider": "apodex",
"mode": "chat",
"source": "https://platform.apodex.ai/docs/models",
"supported_endpoints": [
"/v1/chat/completions",
"/v1/responses",
"/v1/messages"
],
"supports_function_calling": true,
"supports_native_streaming": true,
"supports_prompt_caching": true,
"supports_reasoning": true,
"supports_system_messages": true,
"supports_tool_choice": true,
"supports_vision": false
},
"apodex/apodex-1-1-deep-research": {
"max_tokens": 65536,
"max_input_tokens": 131072,
"max_output_tokens": 65536,
"input_cost_per_token": 5e-06,
"output_cost_per_token": 2e-05,
"litellm_provider": "apodex",
"mode": "chat",
"source": "https://platform.apodex.ai/docs/pricing",
"supported_endpoints": [
"/v1/chat/completions",
"/v1/responses"
],
"supports_function_calling": false,
"supports_native_streaming": true,
"supports_prompt_caching": false,
"supports_reasoning": true,
"supports_response_schema": false,
"supports_system_messages": true,
"supports_tool_choice": false,
"supports_vision": false,
"supports_web_search": true
},
"apodex/apodex-1-1-deep-solve": {
"max_tokens": 65536,
"max_input_tokens": 131072,
"max_output_tokens": 65536,
"input_cost_per_token": 5e-06,
"output_cost_per_token": 2.5e-05,
"litellm_provider": "apodex",
"mode": "chat",
"source": "https://platform.apodex.ai/docs/pricing",
"supported_endpoints": [
"/v1/chat/completions",
"/v1/responses"
],
"supports_function_calling": false,
"supports_native_streaming": true,
"supports_prompt_caching": false,
"supports_reasoning": true,
"supports_response_schema": false,
"supports_system_messages": true,
"supports_tool_choice": false,
"supports_vision": false,
"supports_web_search": true
},
"apodex/apodex-1-1-deep-discover": {
"max_tokens": 262144,
"max_input_tokens": 131072,
"max_output_tokens": 262144,
"input_cost_per_token": 1e-05,
"output_cost_per_token": 0.0001,
"litellm_provider": "apodex",
"mode": "chat",
"source": "https://platform.apodex.ai/docs/pricing",
"supported_endpoints": [
"/v1/chat/completions",
"/v1/responses"
],
"supports_function_calling": false,
"supports_native_streaming": true,
"supports_prompt_caching": false,
"supports_reasoning": true,
"supports_response_schema": false,
"supports_system_messages": true,
"supports_tool_choice": false,
"supports_vision": false,
"supports_web_search": true
},
"apodex/apodex-1-0-deep-research": {
"max_tokens": 16384,
"max_input_tokens": 262144,
"max_output_tokens": 16384,
"input_cost_per_token": 1e-05,
"output_cost_per_token": 4e-05,
"litellm_provider": "apodex",
"mode": "chat",
"source": "https://platform.apodex.ai/docs/pricing",
"supported_endpoints": [
"/v1/chat/completions",
"/v1/responses"
],
"supports_function_calling": false,
"supports_native_streaming": true,
"supports_prompt_caching": false,
"supports_reasoning": true,
"supports_response_schema": false,
"supports_system_messages": true,
"supports_tool_choice": false,
"supports_vision": false,
"supports_web_search": true
},
"apodex/apodex-1-0-deep-solve": {
"max_tokens": 16384,
"max_input_tokens": 262144,
"max_output_tokens": 16384,
"input_cost_per_token": 1e-05,
"output_cost_per_token": 5e-05,
"litellm_provider": "apodex",
"mode": "chat",
"source": "https://platform.apodex.ai/docs/pricing",
"supported_endpoints": [
"/v1/chat/completions",
"/v1/responses"
],
"supports_function_calling": false,
"supports_native_streaming": true,
"supports_prompt_caching": false,
"supports_reasoning": true,
"supports_response_schema": false,
"supports_system_messages": true,
"supports_tool_choice": false,
"supports_vision": false,
"supports_web_search": true
},
"apodex/apodex-1-0-deep-discover": {
"max_tokens": 262144,
"max_input_tokens": 131072,
"max_output_tokens": 262144,
"input_cost_per_token": 1e-05,
"output_cost_per_token": 0.0001,
"litellm_provider": "apodex",
"mode": "chat",
"source": "https://platform.apodex.ai/docs/pricing",
"supported_endpoints": [
"/v1/chat/completions",
"/v1/responses"
],
"supports_function_calling": false,
"supports_native_streaming": true,
"supports_prompt_caching": false,
"supports_reasoning": true,
"supports_response_schema": false,
"supports_system_messages": true,
"supports_tool_choice": false,
"supports_vision": false,
"supports_web_search": true
},
"fallback_generalizations": {
"rules": [
{

View file

@ -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": {

View file

@ -0,0 +1,310 @@
"""
Tests for the Apodex provider (https://platform.apodex.ai/docs).
"""
import json
from pathlib import Path
import httpx
import openai
import pytest
import litellm
from litellm.llms.custom_httpx.http_handler import AsyncHTTPHandler, HTTPHandler
from litellm.types.utils import LlmProviders
from litellm.utils import ProviderConfigManager
REPO_ROOT = Path(__file__).parents[4]
CORE_MODEL = "apodex/apodex-1.1"
DEEP_RESEARCH_MODEL = "apodex/apodex-1-1-deep-research"
CHAT_RESPONSE = {
"id": "chatcmpl-abc123",
"object": "chat.completion",
"created": 1712345678,
"model": "apodex-1.1",
"choices": [
{
"index": 0,
"message": {"role": "assistant", "content": "ok", "reasoning_content": "let me think"},
"finish_reason": "stop",
}
],
"usage": {
"prompt_tokens": 1000,
"completion_tokens": 100,
"total_tokens": 1100,
"prompt_tokens_details": {"cached_tokens": 500},
},
}
STREAM_BODY = (
b'data: {"id":"chatcmpl-abc123","object":"chat.completion.chunk","created":1,"model":"apodex-1.1",'
b'"choices":[{"index":0,"delta":{"content":"ok"},"finish_reason":null}]}\n\n'
b"data: [DONE]\n\n"
)
@pytest.fixture(autouse=True)
def _apodex_env(monkeypatch: pytest.MonkeyPatch):
"""Resolve models against the in-repo cost map, not the published one."""
monkeypatch.setenv("APODEX_API_KEY", "sk-apodex-test")
monkeypatch.setenv("LITELLM_LOCAL_MODEL_COST_MAP", "True")
monkeypatch.setattr(litellm, "model_cost", litellm.get_model_cost_map(url=""))
yield
def _openai_client(captured: dict, *, stream: bool = False) -> openai.OpenAI:
def handler(request: httpx.Request) -> httpx.Response:
captured["url"] = str(request.url)
captured["body"] = json.loads(request.content)
if stream:
return httpx.Response(200, headers={"content-type": "text/event-stream"}, content=STREAM_BODY)
return httpx.Response(200, json=CHAT_RESPONSE)
return openai.OpenAI(
api_key="sk-apodex-test",
base_url="https://api.apodex.ai/v1",
http_client=httpx.Client(transport=httpx.MockTransport(handler)),
)
class TestApodexRegistration:
def test_provider_enum_and_lists(self):
assert LlmProviders.APODEX.value == "apodex"
assert "apodex" in litellm.provider_list
assert "apodex" in litellm.constants.openai_compatible_providers
def test_json_provider_config(self):
from litellm.llms.openai_like.json_loader import JSONProviderRegistry
apodex = JSONProviderRegistry.get("apodex")
assert apodex is not None
assert apodex.base_url == "https://api.apodex.ai/v1"
assert apodex.api_key_env == "APODEX_API_KEY"
assert apodex.api_base_env == "APODEX_API_BASE"
assert apodex.param_mappings["max_completion_tokens"] == "max_tokens"
assert apodex.supported_endpoints == ["/v1/chat/completions", "/v1/responses", "/v1/messages"]
assert JSONProviderRegistry.supports_responses_api("apodex") is True
def test_provider_resolution(self):
model, provider, _, api_base = litellm.get_llm_provider(model=CORE_MODEL)
assert (model, provider, api_base) == ("apodex-1.1", "apodex", "https://api.apodex.ai/v1")
def test_api_base_autodetection(self):
_, provider, api_key, _ = litellm.get_llm_provider(model="apodex-1.1", api_base="https://api.apodex.ai/v1")
assert provider == "apodex"
assert api_key == "sk-apodex-test"
def test_explicit_api_base_and_key_win(self):
_, provider, api_key, api_base = litellm.get_llm_provider(
model=CORE_MODEL, api_base="https://gateway.internal/v1", api_key="sk-override"
)
assert (provider, api_key, api_base) == ("apodex", "sk-override", "https://gateway.internal/v1")
class TestApodexStreamDefault:
"""Apodex defaults `stream` to true, so a non-streaming call must pin it to false.
Regression guard: the OpenAI SDK omits `stream` when it is false, which would make
litellm.completion() receive SSE and fail to parse it.
"""
def test_chat_completion_pins_stream_false(self):
captured: dict = {}
response = litellm.completion(
model=CORE_MODEL,
messages=[{"role": "user", "content": "hi"}],
client=_openai_client(captured),
)
assert captured["url"] == "https://api.apodex.ai/v1/chat/completions"
assert captured["body"]["stream"] is False
assert captured["body"]["model"] == "apodex-1.1"
assert response.choices[0].message.reasoning_content == "let me think"
def test_chat_completion_streaming_sends_stream_true(self):
captured: dict = {}
chunks = list(
litellm.completion(
model=CORE_MODEL,
messages=[{"role": "user", "content": "hi"}],
stream=True,
client=_openai_client(captured, stream=True),
)
)
assert captured["body"]["stream"] is True
assert chunks
def test_user_supplied_extra_body_is_preserved(self):
captured: dict = {}
litellm.completion(
model=CORE_MODEL,
messages=[{"role": "user", "content": "hi"}],
extra_body={"mcp_servers": [{"name": "docs", "url": "https://example.com/mcp"}]},
client=_openai_client(captured),
)
assert captured["body"]["stream"] is False
assert captured["body"]["mcp_servers"] == [{"name": "docs", "url": "https://example.com/mcp"}]
def test_responses_api_pins_stream_false(self):
captured: dict = {}
class CapturingHandler(HTTPHandler):
def post(self, *args, **kwargs):
captured.update(url=kwargs.get("url"), body=kwargs.get("json"))
raise RuntimeError("captured")
with pytest.raises(Exception):
litellm.responses(model=DEEP_RESEARCH_MODEL, input="hi", client=CapturingHandler())
assert captured["url"] == "https://api.apodex.ai/v1/responses"
assert captured["body"]["stream"] is False
assert captured["body"]["model"] == "apodex-1-1-deep-research"
@pytest.mark.asyncio
async def test_responses_api_streaming_sends_stream_true(self):
captured: dict = {}
class CapturingHandler(AsyncHTTPHandler):
async def post(self, *args, **kwargs):
captured.update(body=kwargs.get("json"))
raise RuntimeError("captured")
with pytest.raises(Exception):
await litellm.aresponses(model=DEEP_RESEARCH_MODEL, input="hi", stream=True, client=CapturingHandler())
assert captured["body"]["stream"] is True
def test_flag_is_opt_in_for_other_json_providers(self):
from litellm.llms.openai_like.json_loader import JSONProviderRegistry
pinstripes = JSONProviderRegistry.get("pinstripes")
assert pinstripes is not None
assert "send_explicit_stream_false" not in pinstripes.special_handling
config = ProviderConfigManager.get_provider_chat_config(
model="ps/glm-4.5-air", provider=LlmProviders.PINSTRIPES
)
params = config.map_openai_params({}, {}, "ps/glm-4.5-air", False)
assert "stream" not in params
assert "stream" not in (params.get("extra_body") or {})
class TestApodexToolSupport:
"""Deep research tiers reject OpenAI-style tools; core models accept them."""
def test_deep_research_drops_tool_params(self):
config = ProviderConfigManager.get_provider_chat_config(
model="apodex-1-1-deep-research", provider=LlmProviders.APODEX
)
supported = config.get_supported_openai_params("apodex-1-1-deep-research")
assert "tools" not in supported
assert "tool_choice" not in supported
def test_core_model_keeps_tool_params(self):
config = ProviderConfigManager.get_provider_chat_config(model="apodex-1.1", provider=LlmProviders.APODEX)
supported = config.get_supported_openai_params("apodex-1.1")
assert "tools" in supported
assert "tool_choice" in supported
def test_max_completion_tokens_maps_to_max_tokens(self):
config = ProviderConfigManager.get_provider_chat_config(model="apodex-1.1", provider=LlmProviders.APODEX)
params = config.map_openai_params({"max_completion_tokens": 512}, {}, "apodex-1.1", False)
assert params["max_tokens"] == 512
assert "max_completion_tokens" not in params
class TestApodexAnthropicMessages:
"""Apodex serves POST /v1/messages natively, so the payload is forwarded untranslated."""
def test_native_passthrough_config(self):
config = ProviderConfigManager.get_provider_anthropic_messages_config(
model="apodex-1.1", provider=LlmProviders.APODEX
)
assert config is not None
assert type(config).__name__ == "JSONProviderAnthropicMessagesConfig"
assert (
config.get_complete_url(
api_base=None, api_key=None, model="apodex-1.1", optional_params={}, litellm_params={}
)
== "https://api.apodex.ai/v1/messages"
)
def test_headers_use_provider_api_key(self):
config = ProviderConfigManager.get_provider_anthropic_messages_config(
model="apodex-1.1", provider=LlmProviders.APODEX
)
assert config is not None
headers, _ = config.validate_anthropic_messages_environment(
headers={}, model="apodex-1.1", messages=[], optional_params={}, litellm_params={}
)
assert headers["authorization"] == "Bearer sk-apodex-test"
assert headers["anthropic-version"] == "2023-06-01"
class TestApodexModelMetadata:
@pytest.fixture(scope="class")
def model_cost(self) -> dict:
with open(REPO_ROOT / "model_prices_and_context_window.json") as f:
return json.load(f)
def test_core_model_pricing(self, model_cost: dict):
info = model_cost["apodex/apodex-1.1"]
assert info["litellm_provider"] == "apodex"
assert info["mode"] == "chat"
assert info["max_input_tokens"] == 262144
assert info["input_cost_per_token"] == 3e-07
assert info["cache_read_input_token_cost"] == 3e-08
assert info["output_cost_per_token"] == 3e-06
# Requests over 200K input tokens are billed at 2x across every tier
assert info["input_cost_per_token_above_200k_tokens"] == 6e-07
assert info["cache_read_input_token_cost_above_200k_tokens"] == 6e-08
assert info["output_cost_per_token_above_200k_tokens"] == 6e-06
assert info["supports_prompt_caching"] is True
assert info["supports_function_calling"] is True
assert info["supported_endpoints"] == ["/v1/chat/completions", "/v1/responses", "/v1/messages"]
def test_deep_research_model_pricing(self, model_cost: dict):
info = model_cost["apodex/apodex-1-1-deep-research"]
assert info["max_input_tokens"] == 131072
assert info["max_output_tokens"] == 65536
assert info["input_cost_per_token"] == 5e-06
assert info["output_cost_per_token"] == 2e-05
assert info["supports_function_calling"] is False
assert info["supports_response_schema"] is False
assert info["supports_prompt_caching"] is False
assert info["supports_web_search"] is True
assert info["supported_endpoints"] == ["/v1/chat/completions", "/v1/responses"]
def test_every_apodex_model_is_registered(self, model_cost: dict):
assert {key for key in model_cost if key.startswith("apodex/")} == {
"apodex/apodex-1.1",
"apodex/apodex-1.1-mini",
"apodex/apodex-1-1-deep-research",
"apodex/apodex-1-1-deep-solve",
"apodex/apodex-1-1-deep-discover",
"apodex/apodex-1-0-deep-research",
"apodex/apodex-1-0-deep-solve",
"apodex/apodex-1-0-deep-discover",
}
def test_backup_cost_map_in_sync(self, model_cost: dict):
with open(REPO_ROOT / "litellm" / "model_prices_and_context_window_backup.json") as f:
backup = json.load(f)
for key in (key for key in model_cost if key.startswith("apodex/")):
assert backup[key] == model_cost[key], f"{key} differs between main and backup cost maps"
def test_cost_tracks_cached_input_separately(self):
captured: dict = {}
response = litellm.completion(
model=CORE_MODEL,
messages=[{"role": "user", "content": "hi"}],
client=_openai_client(captured),
)
# 500 fresh input + 500 cached input + 100 output
expected = 500 * 3e-07 + 500 * 3e-08 + 100 * 3e-06
assert litellm.completion_cost(response, model=CORE_MODEL) == pytest.approx(expected)

View file

@ -245,6 +245,63 @@ class TestPinstripes:
assert result["temperature"] == 0.7
class TestTemperatureConstraints:
"""`constraints` in providers.json clamp temperature before the request is sent."""
@staticmethod
def _config(constraints: dict):
from litellm.llms.openai_like.dynamic_config import create_config_class
from litellm.llms.openai_like.json_loader import SimpleProviderConfig
provider = SimpleProviderConfig(
"constrained",
{
"base_url": "https://api.constrained.test/v1",
"api_key_env": "CONSTRAINED_API_KEY",
"constraints": constraints,
},
)
return create_config_class(provider)()
def test_temperature_clamped_to_max(self):
config = self._config({"temperature_max": 1.0})
result = config.map_openai_params({"temperature": 1.8}, {}, "some-model", False)
assert result["temperature"] == 1.0
def test_temperature_clamped_to_min(self):
config = self._config({"temperature_min": 0.1})
result = config.map_openai_params({"temperature": 0.0}, {}, "some-model", False)
assert result["temperature"] == 0.1
def test_temperature_within_range_is_untouched(self):
config = self._config({"temperature_min": 0.1, "temperature_max": 1.0})
result = config.map_openai_params({"temperature": 0.7}, {}, "some-model", False)
assert result["temperature"] == 0.7
def test_temperature_floor_applies_only_when_n_gt_1(self):
config = self._config({"temperature_min_with_n_gt_1": 0.3})
single = config.map_openai_params({"temperature": 0.0, "n": 1}, {}, "some-model", False)
assert single["temperature"] == 0.0
multiple = config.map_openai_params({"temperature": 0.0, "n": 2}, {}, "some-model", False)
assert multiple["temperature"] == 0.3
def test_no_constraints_leaves_temperature_alone(self):
config = self._config({})
result = config.map_openai_params({"temperature": 1.9}, {}, "some-model", False)
assert result["temperature"] == 1.9
def test_caller_optional_params_are_not_mutated(self):
config = self._config({"temperature_max": 1.0})
optional_params = {"temperature": 1.8}
result = config.map_openai_params({"max_tokens": 10}, optional_params, "some-model", False)
assert result["temperature"] == 1.0
assert result["max_tokens"] == 10
assert optional_params == {"temperature": 1.8}
class TestDarkbloom:
def test_darkbloom_json_config_exists(self):
from litellm.llms.openai_like.json_loader import JSONProviderRegistry

View file

@ -27,10 +27,10 @@
"limit": 0
},
"LIT010": {
"limit": 16715
"limit": 16707
},
"LIT011": {
"limit": 5593
"limit": 5589
},
"LIT012": {
"limit": 4519