Merge commit '0c14826f58' into litellm_silent_experiment_spend_row_flag

This commit is contained in:
mateo-berri 2026-10-28 13:04:36 -07:00
commit 054f220e53
32 changed files with 2979 additions and 47 deletions

View file

@ -427,6 +427,7 @@ Set `LITELLM_PROXY_API_BASE` and `LITELLM_PROXY_API_KEY` and every model call th
| [Watsonx Text (`watsonx_text`)](https://docs.litellm.ai/docs/providers/watsonx) | ✅ | ✅ | ✅ | | | | | | | |
| [xAI (`xai`)](https://docs.litellm.ai/docs/providers/xai) | ✅ | ✅ | ✅ | | | | | | | |
| [Xinference (`xinference`)](https://docs.litellm.ai/docs/providers/xinference) | | | | ✅ | | | | | | |
| [ZeroGPU (`zerogpu`)](https://docs.litellm.ai/docs/providers/zerogpu) | ✅ | ✅ | ✅ | | | | | | | |
[**Read the Docs**](https://docs.litellm.ai/docs/)

View file

@ -113,6 +113,7 @@ const OPENAI_COMPATIBLE_PROVIDERS: &[&str] = &[
"meta",
"cognition",
"scx-ai",
"zerogpu",
];
impl ExceptionFamily {

View file

@ -681,6 +681,7 @@ moonshot_models: Set = set()
publicai_models: Set = set()
darkbloom_models: Set = set()
openinfer_models: Final[Set[str]] = set()
zerogpu_models: Final[Set[str]] = set()
v0_models: Set = set()
morph_models: Set = set()
opencode_go_models: Final[set[str]] = set() # mutable-ok: populated when the shared model catalog loads
@ -969,6 +970,8 @@ def _populate_provider_model_sets(model_cost_map: Mapping[str, Mapping[str, obje
darkbloom_models.add(key)
elif value.get("litellm_provider") == "openinfer":
openinfer_models.add(key)
elif value.get("litellm_provider") == "zerogpu":
zerogpu_models.add(key)
elif value.get("litellm_provider") == "v0":
v0_models.add(key)
elif value.get("litellm_provider") == "morph":
@ -1142,6 +1145,7 @@ model_list = list(
| publicai_models
| darkbloom_models
| openinfer_models
| zerogpu_models
| v0_models
| morph_models
| opencode_go_models
@ -1261,6 +1265,7 @@ def _build_models_by_provider() -> dict:
"publicai": publicai_models,
"darkbloom": darkbloom_models,
"openinfer": openinfer_models,
"zerogpu": zerogpu_models,
"v0": v0_models,
"morph": morph_models,
"opencode_go": opencode_go_models,

View file

@ -3,7 +3,6 @@ Handler for transforming /chat/completions api requests to litellm.responses req
"""
import json
import os
from collections.abc import AsyncIterator, Callable, Iterable, Iterator, Mapping, Sequence
from itertools import accumulate, chain
from types import MappingProxyType
@ -43,8 +42,8 @@ from litellm.responses.sse_output_recovery import (
)
from litellm.responses.utils import (
ResponsesAPIRequestUtils,
auto_reasoning_summary,
normalize_responses_api_stream_options,
opt_in_auto_reasoning_summary,
)
from litellm.types.llms.openai import (
ChatCompletionAnnotation,
@ -1745,19 +1744,11 @@ class LiteLLMResponsesTransformationHandler(CompletionTransformationBridge):
**cast(Reasoning, reasoning_effort) # cast-ok: dict is forwarded verbatim to the provider
)
# Check if auto-summary is enabled via flag or environment variable
# Priority: litellm.reasoning_auto_summary flag > LITELLM_REASONING_AUTO_SUMMARY env var
auto_summary_enabled: Final = (
litellm.reasoning_auto_summary or os.getenv("LITELLM_REASONING_AUTO_SUMMARY", "false").lower() == "true"
)
summary: Final = opt_in_auto_reasoning_summary(model, custom_llm_provider)
return (
Reasoning(
effort=reasoning_effort,
summary="detailed" if model is None else auto_reasoning_summary(model, custom_llm_provider),
)
if auto_summary_enabled
else Reasoning(effort=reasoning_effort)
Reasoning(effort=reasoning_effort)
if summary is None
else Reasoning(effort=reasoning_effort, summary=summary)
)
_map_reasoning_effort = map_reasoning_effort

View file

@ -1096,6 +1096,7 @@ openai_compatible_providers: Final[list[str]] = [
"llamafile",
"lm_studio",
"galadriel",
"zerogpu",
"github_copilot", # GitHub Copilot Chat API
"chatgpt", # ChatGPT subscription API
"novita",

View file

@ -2,8 +2,12 @@
Utils used for litellm.transcription() and litellm.atranscription()
"""
import base64
import binascii
import hashlib
import math
import os
from collections.abc import Sequence
from dataclasses import dataclass
from typing import Final
@ -331,6 +335,45 @@ def calculate_request_duration(file: FileTypes) -> float | None:
return None
DEFAULT_AUDIO_TOKEN_ESTIMATE: Final = 300
AUDIO_TOKENS_PER_SECOND: Final = 10
AUDIO_BYTES_PER_TOKEN: Final = 1600
_MAX_AUDIO_ESTIMATE_BYTES: Final = 16 * 1024 * 1024
def _audio_token_rate(model: str) -> int | None:
if model.startswith(
("gemini-", "gemini/gemini-", "vertex_ai/gemini-", "vertex_ai_beta/gemini-", "openrouter/google/gemini-")
):
family: Final = model.rsplit("/", 1)[-1]
return 25 if family.startswith(("gemini-3-", "gemini-3.")) else 32
if model.startswith(("gpt-", "openai/gpt-", "azure/gpt-", "openrouter/openai/gpt-")):
return AUDIO_TOKENS_PER_SECOND
return None
def estimate_audio_tokens(data: str | None, models: Sequence[str] = (), fallback_model: str | None = None) -> int:
"""Estimate audio tokens from container duration within a bounded decoding budget."""
if not data:
return DEFAULT_AUDIO_TOKEN_ESTIMATE
fallback_estimate: Final = max(len(data) * 3 // 4 // AUDIO_BYTES_PER_TOKEN, DEFAULT_AUDIO_TOKEN_ESTIMATE)
padding: Final = 2 if data.endswith("==") else 1 if data.endswith("=") else 0
if len(data) * 3 // 4 - padding > _MAX_AUDIO_ESTIMATE_BYTES:
return fallback_estimate
try:
duration: Final = calculate_request_duration(base64.b64decode(data, validate=True))
except (ValueError, binascii.Error):
return fallback_estimate
if duration is None or not math.isfinite(duration) or duration <= 0:
return fallback_estimate
fallback_rate: Final = _audio_token_rate(fallback_model) if fallback_model is not None else None
rate: Final = max(
(_audio_token_rate(model) or fallback_rate or AUDIO_TOKENS_PER_SECOND for model in models),
default=fallback_rate or AUDIO_TOKENS_PER_SECOND,
)
return max(1, math.ceil(duration * rate))
DEFAULT_SPEECH_MEDIA_TYPE: Final = "audio/mpeg"

View file

@ -9,6 +9,7 @@ import json
import mimetypes
import re
from collections.abc import Callable, Iterable, Iterator, Mapping, Sequence
from dataclasses import dataclass
from graphlib import CycleError, TopologicalSorter
from itertools import chain, groupby, islice
from os import PathLike
@ -236,6 +237,160 @@ _IMAGE_CONTENT_PART_TYPES: Final = frozenset({"image_url", "input_image", "image
_IMAGE_SCAN_MAX_DEPTH: Final = 4
@dataclass(frozen=True, slots=True)
class _TPMMediaProjection:
content: object
media: tuple[Mapping[str, object], ...] = ()
def _tpm_image_part(url: str, detail: object = "auto") -> Mapping[str, object]:
return {"type": "image_url", "image_url": {"url": url, "detail": detail}}
def _tpm_blank_fields(fields: Mapping[str, object], names: tuple[str, ...]) -> Mapping[str, object]:
return {key: "" if key in names and isinstance(value, str) else value for key, value in fields.items()}
def _tpm_file_media(part: Mapping[str, object]) -> _TPMMediaProjection:
from litellm.litellm_core_utils.prompt_templates.factory import parse_mime_type
nested: Final = part.get("type") == "file"
fields: Final = as_str_mapping(part.get("file")) if nested else part
if fields is None:
return _TPMMediaProjection(part)
raw_data: Final = fields.get("file_data")
data: Final = raw_data if isinstance(raw_data, str) else ""
declared: Final = next(
(
value
for key in ("format", "mime_type", "content_type")
if isinstance(value := fields.get(key), str) and value
),
"",
)
mime: Final = (declared or parse_mime_type(data[:256]) or "").split(";", 1)[0].strip().lower()
if not mime.startswith(("image/", "audio/")):
return _TPMMediaProjection(part)
raw_url: Final = fields.get("file_url") or fields.get("file_id")
url: Final = data or (raw_url if isinstance(raw_url, str) else "")
header_end: Final = data.find(",", 0, 256)
encoded: Final = (
data[header_end + 1 :]
if header_end >= 0 and data[:header_end].endswith(";base64")
else ""
if data.startswith(("data:", "https://", "http://", "gs://"))
else data
)
media: Final = (
_tpm_image_part(
url
if not data or url.startswith(("data:", "https://", "http://", "gs://"))
else f"data:{mime};base64,{url}",
fields.get("detail", "auto"),
)
if mime.startswith("image/")
else {"type": "input_audio", "input_audio": {"data": encoded, "format": mime.removeprefix("audio/")}}
)
projected: Final = _tpm_blank_fields(fields, ("file_data", "file_url"))
return _TPMMediaProjection({**part, "file": projected} if nested else projected, (media,))
def _tpm_media_leaf(value: object) -> _TPMMediaProjection:
part: Final = as_str_mapping(value)
if part is None:
return _TPMMediaProjection(value)
kind: Final = part.get("type")
if kind in ("file", "input_file"):
return _tpm_file_media(part)
if kind == "input_audio":
audio: Final = as_str_mapping(part.get("input_audio"))
return _TPMMediaProjection(
{**part, "input_audio": _tpm_blank_fields(audio, ("data",))} if audio is not None else value,
({"type": "input_audio", "input_audio": part.get("input_audio")},),
)
if kind in ("image_url", "input_image", "image", "computer_screenshot"):
image: Final = as_str_mapping(part.get("image_url"))
source: Final = as_str_mapping(part.get("source"))
canonical: Final = _tpm_image_part(
_image_part_url({**part, "type": "input_image"} if kind == "computer_screenshot" else part) or "",
(image or part).get("detail", part.get("detail", "auto")),
)
projected: Final = (
{**part, "image_url": _tpm_blank_fields(image, ("url",))}
if image is not None and kind in ("image_url", "input_image", "computer_screenshot")
else {**part, "source": _tpm_blank_fields(source, ("data", "url"))}
if source is not None and kind == "image" and source.get("type") in ("base64", "url")
else _tpm_blank_fields(part, ("image_url", "url"))
)
return _TPMMediaProjection(projected, (canonical,))
return _TPMMediaProjection(value)
def _tpm_media_list(value: object, split: Callable[[object], _TPMMediaProjection]) -> _TPMMediaProjection:
if not isinstance(value, (list, tuple)):
return _TPMMediaProjection(value)
parts: Final = cast(Sequence[object], value) # cast-ok: finite JSON content arrays
projected: Final = tuple(map(split, parts))
media: Final = tuple(chain.from_iterable(part.media for part in projected))
content: Final = tuple(part.content for part in projected)
return _TPMMediaProjection(parts if not media else content if isinstance(value, tuple) else list(content), media)
def _tpm_replace_field(part: Mapping[str, object], key: str, projection: _TPMMediaProjection) -> _TPMMediaProjection:
return _TPMMediaProjection({**part, key: projection.content} if projection.media else part, projection.media)
def _tpm_document_media(value: object) -> _TPMMediaProjection:
part: Final = as_str_mapping(value)
source: Final = as_str_mapping(part.get("source")) if part is not None else None
if part is None or part.get("type") != "document" or source is None or source.get("type") != "content":
return _tpm_media_leaf(value)
content: Final = _tpm_media_list(source.get("content"), _tpm_media_leaf)
return _tpm_replace_field(part, "source", _tpm_replace_field(source, "content", content))
def _tpm_tool_result_media(value: object) -> _TPMMediaProjection:
part: Final = as_str_mapping(value)
if part is None or part.get("type") != "tool_result":
return _tpm_document_media(value)
return _tpm_replace_field(part, "content", _tpm_media_list(part.get("content"), _tpm_document_media))
def _tpm_code_output_media(value: object) -> _TPMMediaProjection:
part: Final = as_str_mapping(value)
url: Final = part.get("url") if part is not None else None
if part is None or part.get("type") != "image" or not isinstance(url, str) or not url:
return _TPMMediaProjection(value)
return _TPMMediaProjection(_tpm_blank_fields(part, ("url",)), (_tpm_image_part(url),))
def _tpm_input_media(value: object) -> _TPMMediaProjection:
part: Final = as_str_mapping(value)
if part is None:
return _TPMMediaProjection(value)
kind: Final = part.get("type")
if kind in ("function_call_output", "custom_tool_call_output"):
return _tpm_replace_field(part, "output", _tpm_media_list(part.get("output"), _tpm_tool_result_media))
if kind == "computer_call_output":
return _tpm_replace_field(part, "output", _tpm_media_leaf(part.get("output")))
if kind == "code_interpreter_call":
return _tpm_replace_field(part, "outputs", _tpm_media_list(part.get("outputs"), _tpm_code_output_media))
result: Final = part.get("result")
if kind == "image_generation_call" and isinstance(result, str) and result:
return _TPMMediaProjection(
_tpm_blank_fields(part, ("result",)),
(_tpm_image_part("data:application/octet-stream;base64," + result),),
)
if kind == "message" or (kind is None and isinstance(part.get("role"), str)):
return _tpm_replace_field(part, "content", _tpm_media_list(part.get("content"), _tpm_tool_result_media))
return _tpm_tool_result_media(value)
def split_media_for_tpm(value: object) -> tuple[object, tuple[Mapping[str, object], ...]]:
projection: Final = _tpm_media_list(value, _tpm_input_media)
return projection.content, projection.media
def _content_parts_contain_image(parts: Sequence[object]) -> bool:
"""Depth-bounded frontier walk over nested content lists, iterative because the repo bans
recursion; an Anthropic tool_result nests its image parts exactly one level down."""

View file

@ -3,9 +3,12 @@
import base64
import io
import math
import re
import struct
from bisect import bisect_right
from collections.abc import Awaitable, Callable, Iterable, Mapping, Sequence
from itertools import accumulate
from types import MappingProxyType
from typing import TYPE_CHECKING, Final, Literal, cast
import anyio
@ -35,6 +38,7 @@ from litellm.constants import (
TOKEN_COUNTER_MAX_EXACT_CHARS,
)
from litellm.litellm_core_utils.asyncify import asyncify
from litellm.litellm_core_utils.core_helpers import as_str_mapping
from litellm.litellm_core_utils.prompt_templates.chat_template_tools import (
ChatTemplateToolSection,
chat_template_tool_section,
@ -188,22 +192,23 @@ def resize_image_high_res(
# Test the function with the given example
def calculate_tiles_needed(
resized_width,
resized_height,
tile_width=MAX_TILE_WIDTH,
tile_height=MAX_TILE_HEIGHT,
):
resized_width: int,
resized_height: int,
tile_width: int = MAX_TILE_WIDTH,
tile_height: int = MAX_TILE_HEIGHT,
) -> int:
tiles_across: Final = (resized_width + tile_width - 1) // tile_width
tiles_down: Final = (resized_height + tile_height - 1) // tile_height
total_tiles: Final = tiles_across * tiles_down
return total_tiles
def high_detail_image_token_upper_bound(base_tokens: int = 85) -> int:
def high_detail_image_token_upper_bound(base_tokens: int = 85, tile_tokens: int | None = None) -> int:
largest_tile_count: Final = calculate_tiles_needed(
MAX_LONG_SIDE_FOR_IMAGE_HIGH_RES, MAX_SHORT_SIDE_FOR_IMAGE_HIGH_RES
)
return base_tokens + (base_tokens * 2) * largest_tile_count
per_tile: Final = base_tokens * 2 if tile_tokens is None else tile_tokens
return base_tokens + per_tile * largest_tile_count
def _unpack_ints(fmt: str, buffer: bytes) -> tuple[int, ...]:
@ -381,6 +386,219 @@ def calculate_img_tokens(
return total_tokens
_IMAGE_HEADER_BASE64_CHARS: Final = 65_536
_MAX_IMAGE_PATCHES: Final = 30_000
_ANTHROPIC_IMAGE_LIMITS: Final = MappingProxyType(
{"claude-standard": (ANTHROPIC_IMAGE_MAX_LONG_EDGE_PX, 1568), "claude-high": (2576, 4784)}
)
_TILE_IMAGE_TOKEN_COSTS: Final = MappingProxyType(
{
"gpt-4o": (85, 170),
"gpt-4.1": (85, 170),
"gpt-4o-mini": (2833, 5667),
"gpt-5": (70, 140),
"gpt-5.1": (70, 140),
"o1": (75, 150),
"o1-pro": (75, 150),
"o3": (75, 150),
}
)
_PATCH_IMAGE_MULTIPLIERS: Final = MappingProxyType(
{
"gpt-6-astra": 1.2,
"gpt-5.6-sol": 1.2,
"gpt-5.6-terra": 1.2,
"gpt-5.6-luna": 1.2,
"gpt-5.5": 1.2,
"gpt-5.4": 1.2,
"gpt-5.4-mini": 1.2,
"gpt-5.4-nano": 1.2,
"gpt-5.2": 1.2,
"gpt-4.1-mini": 1.62,
}
)
def _inline_image_dimensions(data: str) -> tuple[int, int] | None:
separator: Final = data.find(",", 0, 256)
if not data.startswith("data:") or separator < 0 or not data[:separator].endswith(";base64"):
return None
encoded_header: Final = data[separator + 1 : separator + 1 + _IMAGE_HEADER_BASE64_CHARS]
try:
header: Final = base64.b64decode(encoded_header, validate=True)
image_type: Final = get_image_type(header)
if image_type not in ("png", "gif", "jpeg", "webp"):
return None
if image_type == "webp" and header[12:16] not in (b"VP8X", b"VP8 ", b"VP8L"):
return None
dimensions: Final = _image_dimensions_from_bytes(header)
except (ValueError, TypeError, struct.error):
return None
width, height = dimensions
return (width, height) if width > 0 and height > 0 else None
def _patch_image_limits(model: str, detail: str) -> tuple[int, int | None] | None:
if model in ("gpt-5.2", "gpt-4.1-mini"):
return None if detail == "original" else (2048, 6144)
original_default: Final = model in ("gpt-6-astra", "gpt-5.6-sol", "gpt-5.6-terra", "gpt-5.6-luna", "gpt-5.5")
effective_detail: Final = ("original" if original_default else "high") if detail == "auto" else detail
if effective_detail == "low":
return (2048, 6144) if model in ("gpt-5.4", "gpt-5.4-mini", "gpt-5.4-nano") else (512, None)
if effective_detail == "high":
return (65_535 if model == "gpt-6-astra" else 2048, 2500)
return (
(65_535, None) if model in ("gpt-6-astra", "gpt-5.6-sol", "gpt-5.6-terra", "gpt-5.6-luna") else (6000, 10_000)
)
def _image_patch_count(width: int, height: int, max_dimension: int, budget: int | None) -> int:
dimension_scale: Final = min(1.0, max_dimension / max(width, height))
fitted_width: Final = max(1, round(width * dimension_scale))
fitted_height: Final = max(1, round(height * dimension_scale))
patches: Final = math.ceil(fitted_width / 32) * math.ceil(fitted_height / 32)
if budget is None or patches <= budget:
return patches
shrink: Final = math.sqrt(32**2 * budget / (fitted_width * fitted_height))
width_patches: Final = fitted_width * shrink / 32
height_patches: Final = fitted_height * shrink / 32
aligned_shrink: Final = shrink * min(
math.floor(width_patches) / width_patches, math.floor(height_patches) / height_patches
)
resized_width: Final = max(1, math.floor(fitted_width * aligned_shrink))
resized_height: Final = max(1, math.floor(fitted_height * aligned_shrink))
return math.ceil(resized_width / 32) * math.ceil(resized_height / 32)
def _image_policy_model_name(model: str) -> str | None:
provider, separator, provider_model = model.partition("/")
if not separator:
return model
if provider == "openai":
return provider_model
if provider == "openrouter":
return provider_model.removeprefix("openai/") if provider_model.startswith("openai/") else None
if provider not in ("azure", "azure_ai"):
return None
catalog_entry: Final = as_str_mapping(
cast( # cast-ok: read-only view of the legacy string-keyed registry; as_str_mapping checks the entry
Mapping[str, object], litellm.model_cost
).get(model)
)
if catalog_entry is not None and catalog_entry.get("litellm_provider") in ("azure", "azure_ai"):
return provider_model.rsplit("/", 1)[-1]
return provider_model.removeprefix("responses/").removeprefix("o_series/").removeprefix("openai/")
def _anthropic_image_family(model: str) -> str | None:
from litellm.utils import get_potential_model_names
provider, separator, _ = model.partition("/")
if separator and provider not in (
"anthropic",
"bedrock",
"bedrock_converse",
"bedrock_mantle",
"vertex_ai",
"vertex_ai_beta",
"azure_ai",
"openrouter",
):
return None
candidates: Final = get_potential_model_names(model, provider if separator else "anthropic")
catalog: Final = cast(Mapping[str, object], litellm.model_cost) # cast-ok: read-only view of the legacy registry
for candidate in dict.fromkeys(candidates.values()):
if not isinstance(candidate, str):
continue
if (entry := as_str_mapping(catalog.get(candidate))) is None or entry.get("supports_vision") is not True:
continue
if (
version := re.search(
r"(?:^|[./])claude-(?:(?:haiku|sonnet|opus)-)?(\d+)(?:[-.](\d{1,2})(?!\d))?", candidate
)
) is None:
continue
major, minor = int(version[1]), int(version[2] or 0)
return "claude-high" if (major, minor) >= (4, 7) else "claude-standard"
return None
def _anthropic_image_tokens(dimensions: tuple[int, int] | None, limits: tuple[int, int]) -> int:
max_edge, token_budget = limits
if dimensions is None:
return token_budget
long_edge, short_edge = max(dimensions), min(dimensions)
aspect_ratio: Final = long_edge / short_edge
def tokens_at_edge(edge: int) -> int:
return math.ceil(edge / 28) * math.ceil(max(round(edge / aspect_ratio), 1) / 28)
fitted_edge: Final = bisect_right(range(1, min(long_edge, max_edge) + 1), token_budget, key=tokens_at_edge)
return tokens_at_edge(max(fitted_edge, 1))
def _image_model_family(model: str) -> str | None:
provider_model: Final = _image_policy_model_name(model)
if provider_model is None:
return _anthropic_image_family(model)
family: Final = re.sub(r"-\d{4}-\d{2}-\d{2}$", "", provider_model)
canonical: Final = litellm.azure_llms.get(f"azure/{family}", family) if model.startswith("azure/") else family
return (
canonical
if canonical in _TILE_IMAGE_TOKEN_COSTS or canonical in _PATCH_IMAGE_MULTIPLIERS
else _anthropic_image_family(model)
)
def _image_policy_tokens(family: str | None, dimensions: tuple[int, int] | None, detail: str) -> int:
if family is None:
return DEFAULT_IMAGE_TOKEN_COUNT
anthropic_limits: Final = _ANTHROPIC_IMAGE_LIMITS.get(family)
if anthropic_limits is not None:
return _anthropic_image_tokens(dimensions, anthropic_limits)
tile_costs: Final = _TILE_IMAGE_TOKEN_COSTS.get(family)
multiplier: Final = _PATCH_IMAGE_MULTIPLIERS.get(family)
if tile_costs is not None:
base_tokens, tile_tokens = tile_costs
if detail == "original":
return DEFAULT_IMAGE_TOKEN_COUNT
if detail == "low":
return base_tokens
if dimensions is None:
return high_detail_image_token_upper_bound(base_tokens, tile_tokens)
width, height = dimensions
scale: Final = min(1.0, 2048 / max(width, height), 768 / min(width, height))
tiles: Final = calculate_tiles_needed(max(1, math.floor(width * scale)), max(1, math.floor(height * scale)))
return base_tokens + tiles * tile_tokens
limits: Final = _patch_image_limits(family, detail)
if limits is None or multiplier is None:
return DEFAULT_IMAGE_TOKEN_COUNT
max_dimension, patch_budget = limits
patches: Final = (
min(math.ceil(max_dimension / 32) ** 2, patch_budget if patch_budget is not None else _MAX_IMAGE_PATCHES)
if dimensions is None
else _image_patch_count(*dimensions, max_dimension, patch_budget)
)
return math.ceil(patches * multiplier)
def estimate_image_tokens(
model: str | Sequence[str], data: str, detail: str = "auto", *, fallback_model: str | None = None
) -> int:
"""Estimate image policies from at most 48 KiB of inline headers, without fetching media."""
if detail not in ("low", "high", "auto", "original"):
return DEFAULT_IMAGE_TOKEN_COUNT
models: Final = (model,) if isinstance(model, str) else model
fallback_family: Final = _image_model_family(fallback_model) if fallback_model is not None else None
families: Final = tuple(
dict.fromkeys(_image_model_family(candidate) or fallback_family for candidate in models or ("",))
)
if all(family is None for family in families):
return DEFAULT_IMAGE_TOKEN_COUNT
dimensions: Final = _inline_image_dimensions(data)
return max(_image_policy_tokens(family, dimensions, detail) for family in families)
TokenCounterFunction = Callable[[str], int]
"""
Type for a function that counts tokens in a string.

View file

@ -58,6 +58,12 @@
"max_completion_tokens": "max_tokens"
}
},
"zerogpu": {
"base_url": "https://api.zerogpu.ai/v1",
"api_key_env": "ZEROGPU_API_KEY",
"api_base_env": "ZEROGPU_API_BASE",
"supported_endpoints": ["/v1/chat/completions", "/v1/responses"]
},
"chutes": {
"base_url": "https://llm.chutes.ai/v1/",
"api_key_env": "CHUTES_API_KEY",

View file

@ -6124,7 +6124,18 @@ def completion(
optional_params, rs_val = strip_reasoning_summary_aliases_from_optional_params(optional_params)
if isinstance(resolved_reasoning_effort, dict) and "summary" in resolved_reasoning_effort:
if custom_llm_provider == "openai" and isinstance(resolved_reasoning_effort, dict):
from litellm.responses.utils import openai_bridge_reasoning
optional_params["reasoning_effort"] = openai_bridge_reasoning(
requested=resolved_reasoning_effort,
effort_kept="reasoning_effort" in optional_params,
summary_alias=rs_val,
model=model,
drop_params=drop_unsupported_reasoning,
additional_drop_params=routing_kwargs.get("additional_drop_params"),
)
elif isinstance(resolved_reasoning_effort, dict) and "summary" in resolved_reasoning_effort:
optional_params["reasoning_effort"] = resolved_reasoning_effort
elif rs_val is not None:
eff: Final = optional_params.get("reasoning_effort", resolved_reasoning_effort)

View file

@ -35598,6 +35598,13 @@
"output_cost_per_token_batches": 5e-06,
"output_cost_per_token_flex": 5e-06,
"output_cost_per_token_priority": 2e-05,
"reasoning_contexts": [
"auto",
"current_turn"
],
"reasoning_modes": [
"standard"
],
"search_context_cost_per_query": {
"search_context_size_high": 0.01,
"search_context_size_low": 0.01,
@ -35675,6 +35682,13 @@
"mode": "chat",
"output_cost_per_token": 1e-05,
"output_cost_per_token_priority": 2e-05,
"reasoning_contexts": [
"auto",
"current_turn"
],
"reasoning_modes": [
"standard"
],
"search_context_cost_per_query": {
"search_context_size_high": 0.01,
"search_context_size_low": 0.01,
@ -35851,6 +35865,13 @@
"mode": "chat",
"output_cost_per_token": 1e-05,
"output_cost_per_token_priority": 2e-05,
"reasoning_contexts": [
"auto",
"current_turn"
],
"reasoning_modes": [
"standard"
],
"search_context_cost_per_query": {
"search_context_size_high": 0.01,
"search_context_size_low": 0.01,
@ -35902,6 +35923,13 @@
"mode": "chat",
"output_cost_per_token": 1.4e-05,
"output_cost_per_token_priority": 2.8e-05,
"reasoning_contexts": [
"auto",
"current_turn"
],
"reasoning_modes": [
"standard"
],
"search_context_cost_per_query": {
"search_context_size_high": 0.01,
"search_context_size_low": 0.01,
@ -36017,6 +36045,13 @@
"mode": "chat",
"output_cost_per_token": 1.4e-05,
"output_cost_per_token_priority": 2.8e-05,
"reasoning_contexts": [
"auto",
"current_turn"
],
"reasoning_modes": [
"standard"
],
"search_context_cost_per_query": {
"search_context_size_high": 0.01,
"search_context_size_low": 0.01,
@ -36066,6 +36101,13 @@
"mode": "responses",
"output_cost_per_token": 0.000168,
"output_cost_per_token_batches": 8.4e-05,
"reasoning_contexts": [
"auto",
"current_turn"
],
"reasoning_modes": [
"pro"
],
"search_context_cost_per_query": {
"search_context_size_high": 0.01,
"search_context_size_low": 0.01,
@ -36107,6 +36149,13 @@
"mode": "responses",
"output_cost_per_token": 0.000168,
"output_cost_per_token_batches": 8.4e-05,
"reasoning_contexts": [
"auto",
"current_turn"
],
"reasoning_modes": [
"pro"
],
"search_context_cost_per_query": {
"search_context_size_high": 0.01,
"search_context_size_low": 0.01,
@ -36194,6 +36243,15 @@
"output_cost_per_token_flex": 2.5e-05,
"output_cost_per_token_priority": 0.0001,
"output_cost_per_token_ultrafast": 0.0003,
"reasoning_contexts": [
"auto",
"current_turn",
"all_turns"
],
"reasoning_modes": [
"standard",
"pro"
],
"regional_processing_uplift_multiplier_eu": 1.1,
"regional_processing_uplift_multiplier_us": 1.1,
"search_context_cost_per_query": {
@ -36278,6 +36336,15 @@
"output_cost_per_token_above_272k_tokens_batches": 7.5e-06,
"output_cost_per_token_flex": 5e-06,
"output_cost_per_token_priority": 2e-05,
"reasoning_contexts": [
"auto",
"current_turn",
"all_turns"
],
"reasoning_modes": [
"standard",
"pro"
],
"regional_processing_uplift_multiplier_eu": 1.1,
"regional_processing_uplift_multiplier_us": 1.1,
"search_context_cost_per_query": {
@ -36370,6 +36437,15 @@
"output_cost_per_token_above_272k_tokens_batches": 3.75e-07,
"output_cost_per_token_flex": 2.5e-07,
"output_cost_per_token_priority": 1e-06,
"reasoning_contexts": [
"auto",
"current_turn",
"all_turns"
],
"reasoning_modes": [
"standard",
"pro"
],
"regional_processing_uplift_multiplier_eu": 1.1,
"regional_processing_uplift_multiplier_us": 1.1,
"search_context_cost_per_query": {
@ -36453,6 +36529,15 @@
"output_cost_per_token_above_272k_tokens_batches": 1.5e-05,
"output_cost_per_token_flex": 1e-05,
"output_cost_per_token_priority": 4e-05,
"reasoning_contexts": [
"auto",
"current_turn",
"all_turns"
],
"reasoning_modes": [
"standard",
"pro"
],
"regional_processing_uplift_multiplier_eu": 1.1,
"regional_processing_uplift_multiplier_us": 1.1,
"search_context_cost_per_query": {
@ -36534,6 +36619,15 @@
"output_cost_per_token_above_272k_tokens_batches": 1.5e-05,
"output_cost_per_token_flex": 1e-05,
"output_cost_per_token_priority": 4e-05,
"reasoning_contexts": [
"auto",
"current_turn",
"all_turns"
],
"reasoning_modes": [
"standard",
"pro"
],
"regional_processing_uplift_multiplier_eu": 1.1,
"regional_processing_uplift_multiplier_us": 1.1,
"search_context_cost_per_query": {
@ -36617,6 +36711,15 @@
"output_cost_per_token_above_272k_tokens_batches": 9e-06,
"output_cost_per_token_flex": 6e-06,
"output_cost_per_token_priority": 2.4e-05,
"reasoning_contexts": [
"auto",
"current_turn",
"all_turns"
],
"reasoning_modes": [
"standard",
"pro"
],
"regional_processing_uplift_multiplier_eu": 1.1,
"regional_processing_uplift_multiplier_us": 1.1,
"search_context_cost_per_query": {
@ -36699,6 +36802,15 @@
"output_cost_per_token_above_272k_tokens_batches": 9e-07,
"output_cost_per_token_flex": 6e-07,
"output_cost_per_token_priority": 2.4e-06,
"reasoning_contexts": [
"auto",
"current_turn",
"all_turns"
],
"reasoning_modes": [
"standard",
"pro"
],
"regional_processing_uplift_multiplier_eu": 1.1,
"regional_processing_uplift_multiplier_us": 1.1,
"search_context_cost_per_query": {
@ -36936,6 +37048,14 @@
"reasoning_effort_levels": [
"medium"
],
"reasoning_contexts": [
"auto",
"current_turn",
"all_turns"
],
"reasoning_modes": [
"standard"
],
"source": "https://developers.openai.com/api/docs/pricing",
"supported_endpoints": [
"/v1/chat/completions",
@ -36983,6 +37103,14 @@
"output_cost_per_token_batches": 1.5e-05,
"output_cost_per_token_above_272k_tokens_batches": 2.25e-05,
"output_cost_per_token_priority": 7.5e-05,
"reasoning_contexts": [
"auto",
"current_turn",
"all_turns"
],
"reasoning_modes": [
"standard"
],
"regional_processing_uplift_multiplier_eu": 1.1,
"regional_processing_uplift_multiplier_us": 1.1,
"search_context_cost_per_query": {
@ -37045,6 +37173,14 @@
"output_cost_per_token_batches": 1.5e-05,
"output_cost_per_token_above_272k_tokens_batches": 2.25e-05,
"output_cost_per_token_priority": 7.5e-05,
"reasoning_contexts": [
"auto",
"current_turn",
"all_turns"
],
"reasoning_modes": [
"standard"
],
"regional_processing_uplift_multiplier_eu": 1.1,
"regional_processing_uplift_multiplier_us": 1.1,
"search_context_cost_per_query": {
@ -37099,6 +37235,14 @@
"output_cost_per_token_flex": 9e-05,
"output_cost_per_token_batches": 9e-05,
"output_cost_per_token_above_272k_tokens_batches": 0.000135,
"reasoning_contexts": [
"auto",
"current_turn",
"all_turns"
],
"reasoning_modes": [
"pro"
],
"regional_processing_uplift_multiplier_eu": 1.1,
"regional_processing_uplift_multiplier_us": 1.1,
"search_context_cost_per_query": {
@ -37150,6 +37294,14 @@
"output_cost_per_token_flex": 9e-05,
"output_cost_per_token_batches": 9e-05,
"output_cost_per_token_above_272k_tokens_batches": 0.000135,
"reasoning_contexts": [
"auto",
"current_turn",
"all_turns"
],
"reasoning_modes": [
"pro"
],
"regional_processing_uplift_multiplier_eu": 1.1,
"regional_processing_uplift_multiplier_us": 1.1,
"search_context_cost_per_query": {
@ -37209,6 +37361,14 @@
"output_cost_per_token_batches": 7.5e-06,
"output_cost_per_token_above_272k_tokens_batches": 1.125e-05,
"output_cost_per_token_priority": 3e-05,
"reasoning_contexts": [
"auto",
"current_turn",
"all_turns"
],
"reasoning_modes": [
"standard"
],
"regional_processing_uplift_multiplier_eu": 1.1,
"regional_processing_uplift_multiplier_us": 1.1,
"supported_endpoints": [
@ -37266,6 +37426,14 @@
"output_cost_per_token_batches": 7.5e-06,
"output_cost_per_token_above_272k_tokens_batches": 1.125e-05,
"output_cost_per_token_priority": 3e-05,
"reasoning_contexts": [
"auto",
"current_turn",
"all_turns"
],
"reasoning_modes": [
"standard"
],
"regional_processing_uplift_multiplier_eu": 1.1,
"regional_processing_uplift_multiplier_us": 1.1,
"supported_endpoints": [
@ -37315,6 +37483,14 @@
"output_cost_per_token_flex": 9e-05,
"output_cost_per_token_batches": 9e-05,
"output_cost_per_token_above_272k_tokens_batches": 0.000135,
"reasoning_contexts": [
"auto",
"current_turn",
"all_turns"
],
"reasoning_modes": [
"pro"
],
"regional_processing_uplift_multiplier_eu": 1.1,
"regional_processing_uplift_multiplier_us": 1.1,
"search_context_cost_per_query": {
@ -37367,6 +37543,14 @@
"output_cost_per_token_flex": 9e-05,
"output_cost_per_token_batches": 9e-05,
"output_cost_per_token_above_272k_tokens_batches": 0.000135,
"reasoning_contexts": [
"auto",
"current_turn",
"all_turns"
],
"reasoning_modes": [
"pro"
],
"regional_processing_uplift_multiplier_eu": 1.1,
"regional_processing_uplift_multiplier_us": 1.1,
"search_context_cost_per_query": {
@ -37421,6 +37605,14 @@
"output_cost_per_token_flex": 2.25e-06,
"output_cost_per_token_batches": 2.25e-06,
"output_cost_per_token_priority": 9e-06,
"reasoning_contexts": [
"auto",
"current_turn",
"all_turns"
],
"reasoning_modes": [
"standard"
],
"regional_processing_uplift_multiplier_eu": 1.1,
"regional_processing_uplift_multiplier_us": 1.1,
"search_context_cost_per_query": {
@ -37475,6 +37667,14 @@
"output_cost_per_token_flex": 2.25e-06,
"output_cost_per_token_batches": 2.25e-06,
"output_cost_per_token_priority": 9e-06,
"reasoning_contexts": [
"auto",
"current_turn",
"all_turns"
],
"reasoning_modes": [
"standard"
],
"regional_processing_uplift_multiplier_eu": 1.1,
"regional_processing_uplift_multiplier_us": 1.1,
"search_context_cost_per_query": {
@ -37526,6 +37726,14 @@
"output_cost_per_token": 1.25e-06,
"output_cost_per_token_flex": 6.25e-07,
"output_cost_per_token_batches": 6.25e-07,
"reasoning_contexts": [
"auto",
"current_turn",
"all_turns"
],
"reasoning_modes": [
"standard"
],
"regional_processing_uplift_multiplier_eu": 1.1,
"regional_processing_uplift_multiplier_us": 1.1,
"search_context_cost_per_query": {
@ -37578,6 +37786,14 @@
"output_cost_per_token": 1.25e-06,
"output_cost_per_token_flex": 6.25e-07,
"output_cost_per_token_batches": 6.25e-07,
"reasoning_contexts": [
"auto",
"current_turn",
"all_turns"
],
"reasoning_modes": [
"standard"
],
"regional_processing_uplift_multiplier_eu": 1.1,
"regional_processing_uplift_multiplier_us": 1.1,
"search_context_cost_per_query": {
@ -37624,6 +37840,13 @@
"mode": "responses",
"output_cost_per_token": 0.00012,
"output_cost_per_token_batches": 6e-05,
"reasoning_contexts": [
"auto",
"current_turn"
],
"reasoning_modes": [
"pro"
],
"search_context_cost_per_query": {
"search_context_size_high": 0.01,
"search_context_size_low": 0.01,
@ -37666,6 +37889,13 @@
"mode": "responses",
"output_cost_per_token": 0.00012,
"output_cost_per_token_batches": 6e-05,
"reasoning_contexts": [
"auto",
"current_turn"
],
"reasoning_modes": [
"pro"
],
"search_context_cost_per_query": {
"search_context_size_high": 0.01,
"search_context_size_low": 0.01,
@ -37716,6 +37946,13 @@
"output_cost_per_token_batches": 5e-06,
"output_cost_per_token_flex": 5e-06,
"output_cost_per_token_priority": 2e-05,
"reasoning_contexts": [
"auto",
"current_turn"
],
"reasoning_modes": [
"standard"
],
"search_context_cost_per_query": {
"search_context_size_high": 0.01,
"search_context_size_low": 0.01,
@ -37826,6 +38063,13 @@
"mode": "responses",
"output_cost_per_token": 1.4e-05,
"output_cost_per_token_priority": 2.8e-05,
"reasoning_contexts": [
"auto",
"current_turn"
],
"reasoning_modes": [
"standard"
],
"search_context_cost_per_query": {
"search_context_size_high": 0.01,
"search_context_size_low": 0.01,
@ -37908,6 +38152,13 @@
"output_cost_per_token_batches": 1e-06,
"output_cost_per_token_flex": 1e-06,
"output_cost_per_token_priority": 3.6e-06,
"reasoning_contexts": [
"auto",
"current_turn"
],
"reasoning_modes": [
"standard"
],
"search_context_cost_per_query": {
"search_context_size_high": 0.01,
"search_context_size_low": 0.01,
@ -37959,6 +38210,13 @@
"output_cost_per_token_batches": 1e-06,
"output_cost_per_token_flex": 1e-06,
"output_cost_per_token_priority": 3.6e-06,
"reasoning_contexts": [
"auto",
"current_turn"
],
"reasoning_modes": [
"standard"
],
"search_context_cost_per_query": {
"search_context_size_high": 0.01,
"search_context_size_low": 0.01,
@ -38007,6 +38265,13 @@
"output_cost_per_token": 4e-07,
"output_cost_per_token_batches": 2e-07,
"output_cost_per_token_flex": 2e-07,
"reasoning_contexts": [
"auto",
"current_turn"
],
"reasoning_modes": [
"standard"
],
"search_context_cost_per_query": {
"search_context_size_high": 0.01,
"search_context_size_low": 0.01,
@ -38055,6 +38320,13 @@
"output_cost_per_token": 4e-07,
"output_cost_per_token_batches": 2e-07,
"output_cost_per_token_flex": 2e-07,
"reasoning_contexts": [
"auto",
"current_turn"
],
"reasoning_modes": [
"standard"
],
"search_context_cost_per_query": {
"search_context_size_high": 0.01,
"search_context_size_low": 0.01,
@ -43821,6 +44093,13 @@
"output_cost_per_token_batches": 4e-06,
"output_cost_per_token_flex": 4e-06,
"output_cost_per_token_priority": 1.4e-05,
"reasoning_contexts": [
"auto",
"current_turn"
],
"reasoning_modes": [
"standard"
],
"search_context_cost_per_query": {
"search_context_size_high": 0.01,
"search_context_size_low": 0.01,
@ -43929,6 +44208,13 @@
"output_cost_per_token_batches": 4e-06,
"output_cost_per_token_flex": 4e-06,
"output_cost_per_token_priority": 1.4e-05,
"reasoning_contexts": [
"auto",
"current_turn"
],
"reasoning_modes": [
"standard"
],
"search_context_cost_per_query": {
"search_context_size_high": 0.01,
"search_context_size_low": 0.01,
@ -44008,6 +44294,13 @@
"mode": "responses",
"output_cost_per_token": 8e-05,
"output_cost_per_token_batches": 4e-05,
"reasoning_contexts": [
"auto",
"current_turn"
],
"reasoning_modes": [
"pro"
],
"search_context_cost_per_query": {
"search_context_size_high": 0.01,
"search_context_size_low": 0.01,
@ -44045,6 +44338,13 @@
"mode": "responses",
"output_cost_per_token": 8e-05,
"output_cost_per_token_batches": 4e-05,
"reasoning_contexts": [
"auto",
"current_turn"
],
"reasoning_modes": [
"pro"
],
"search_context_cost_per_query": {
"search_context_size_high": 0.01,
"search_context_size_low": 0.01,
@ -65863,6 +66163,180 @@
"supports_reasoning": true,
"supports_tool_choice": true
},
"zerogpu/deepseek-v4.1-flash": {
"cache_read_input_token_cost": 3e-09,
"input_cost_per_token": 3e-07,
"litellm_provider": "zerogpu",
"max_input_tokens": 1048576,
"max_output_tokens": 65536,
"mode": "chat",
"output_cost_per_token": 1.2e-06,
"source": "https://docs.zerogpu.ai/docs/model-catalog",
"supports_function_calling": true,
"supports_prompt_caching": true,
"supports_reasoning": true,
"supports_response_schema": true,
"supports_tool_choice": true
},
"zerogpu/glm-5.3-flash": {
"cache_read_input_token_cost": 1.5e-08,
"input_cost_per_token": 1e-07,
"litellm_provider": "zerogpu",
"max_input_tokens": 1048576,
"max_output_tokens": 32768,
"mode": "chat",
"output_cost_per_token": 3.5e-07,
"source": "https://docs.zerogpu.ai/docs/model-catalog",
"supports_function_calling": true,
"supports_prompt_caching": true,
"supports_reasoning": true,
"supports_response_schema": true,
"supports_tool_choice": true
},
"zerogpu/gpt-4.1-mini": {
"cache_read_input_token_cost": 1e-07,
"input_cost_per_token": 4e-07,
"litellm_provider": "zerogpu",
"max_input_tokens": 1047576,
"max_output_tokens": 32768,
"mode": "chat",
"output_cost_per_token": 1.6e-06,
"source": "https://docs.zerogpu.ai/docs/model-catalog",
"supports_function_calling": true,
"supports_prompt_caching": true,
"supports_response_schema": true,
"supports_tool_choice": true
},
"zerogpu/gpt-5.4-nano": {
"cache_read_input_token_cost": 2e-08,
"input_cost_per_token": 2e-07,
"litellm_provider": "zerogpu",
"max_input_tokens": 272000,
"max_output_tokens": 128000,
"mode": "chat",
"output_cost_per_token": 1.25e-06,
"source": "https://docs.zerogpu.ai/docs/model-catalog",
"supports_function_calling": true,
"supports_prompt_caching": true,
"supports_reasoning": true,
"supports_response_schema": true,
"supports_tool_choice": true
},
"zerogpu/gpt-5.6-luna": {
"cache_read_input_token_cost": 2e-07,
"input_cost_per_token": 2e-07,
"litellm_provider": "zerogpu",
"max_input_tokens": 272000,
"max_output_tokens": 128000,
"mode": "chat",
"output_cost_per_token": 1.2e-06,
"source": "https://docs.zerogpu.ai/docs/model-catalog",
"supports_function_calling": true,
"supports_prompt_caching": true,
"supports_reasoning": true,
"supports_response_schema": true,
"supports_tool_choice": true
},
"zerogpu/gpt-6-luna": {
"cache_read_input_token_cost": 1e-08,
"input_cost_per_token": 1e-07,
"litellm_provider": "zerogpu",
"max_input_tokens": 272000,
"max_output_tokens": 128000,
"mode": "chat",
"output_cost_per_token": 5e-07,
"source": "https://docs.zerogpu.ai/docs/model-catalog",
"supports_function_calling": true,
"supports_prompt_caching": true,
"supports_reasoning": true,
"supports_response_schema": true,
"supports_tool_choice": true,
"supports_vision": true
},
"zerogpu/gpt-oss-120b": {
"cache_read_input_token_cost": 3e-08,
"input_cost_per_token": 1.5e-07,
"litellm_provider": "zerogpu",
"max_input_tokens": 128000,
"max_output_tokens": 32768,
"mode": "chat",
"output_cost_per_token": 6e-07,
"source": "https://docs.zerogpu.ai/docs/model-catalog",
"supports_function_calling": true,
"supports_reasoning": true,
"supports_response_schema": true,
"supports_tool_choice": true
},
"zerogpu/LFM2.5-1.2B-Instruct": {
"input_cost_per_token": 2e-08,
"litellm_provider": "zerogpu",
"max_input_tokens": 32768,
"max_output_tokens": 8192,
"mode": "chat",
"output_cost_per_token": 5e-08,
"source": "https://docs.zerogpu.ai/docs/model-catalog"
},
"zerogpu/LFM2.5-1.2B-Thinking": {
"input_cost_per_token": 2e-08,
"litellm_provider": "zerogpu",
"max_input_tokens": 32768,
"max_output_tokens": 8192,
"mode": "chat",
"output_cost_per_token": 5e-08,
"source": "https://docs.zerogpu.ai/docs/model-catalog"
},
"zerogpu/llama-3.1-8b-instruct-fast": {
"cache_read_input_token_cost": 2.5e-08,
"input_cost_per_token": 1.5e-07,
"litellm_provider": "zerogpu",
"max_input_tokens": 131072,
"max_output_tokens": 8192,
"mode": "chat",
"output_cost_per_token": 2.8e-07,
"source": "https://docs.zerogpu.ai/docs/model-catalog",
"supports_function_calling": true,
"supports_response_schema": true,
"supports_tool_choice": true
},
"zerogpu/llama-3.2-3b-instruct-fast": {
"cache_read_input_token_cost": 2.5e-08,
"input_cost_per_token": 5e-08,
"litellm_provider": "zerogpu",
"max_input_tokens": 131072,
"max_output_tokens": 8192,
"mode": "chat",
"output_cost_per_token": 3e-07,
"source": "https://docs.zerogpu.ai/docs/model-catalog"
},
"zerogpu/qwen3-30b-a3b-fp8": {
"cache_read_input_token_cost": 5e-08,
"input_cost_per_token": 1e-07,
"litellm_provider": "zerogpu",
"max_input_tokens": 32768,
"max_output_tokens": 16384,
"mode": "chat",
"output_cost_per_token": 4.5e-07,
"source": "https://docs.zerogpu.ai/docs/model-catalog",
"supports_function_calling": true,
"supports_reasoning": true,
"supports_response_schema": true,
"supports_tool_choice": true
},
"zerogpu/qwen3.6-35b-a3b": {
"cache_read_input_token_cost": 2e-07,
"input_cost_per_token": 2e-07,
"litellm_provider": "zerogpu",
"max_input_tokens": 262144,
"max_output_tokens": 16384,
"mode": "chat",
"output_cost_per_token": 1.5e-06,
"source": "https://docs.zerogpu.ai/docs/model-catalog",
"supports_function_calling": true,
"supports_reasoning": true,
"supports_response_schema": true,
"supports_tool_choice": true,
"supports_vision": true
},
"together_ai/Qwen/Qwen3.8-Flash": {
"input_cost_per_token": 1.5e-07,
"litellm_provider": "together_ai",
@ -73288,6 +73762,15 @@
"output_cost_per_token_flex": 5e-06,
"output_cost_per_token_priority": 2e-05,
"output_cost_per_token_ultrafast": 6e-05,
"reasoning_contexts": [
"auto",
"current_turn",
"all_turns"
],
"reasoning_modes": [
"standard",
"pro"
],
"regional_processing_uplift_multiplier_eu": 1.1,
"regional_processing_uplift_multiplier_us": 1.1,
"search_context_cost_per_query": {
@ -76787,6 +77270,15 @@
"output_cost_per_token_batches": 2.5e-06,
"output_cost_per_token_flex": 2.5e-06,
"output_cost_per_token_priority": 1e-05,
"reasoning_contexts": [
"auto",
"current_turn",
"all_turns"
],
"reasoning_modes": [
"standard",
"pro"
],
"regional_processing_uplift_multiplier_eu": 1.1,
"regional_processing_uplift_multiplier_us": 1.1,
"search_context_cost_per_query": {"search_context_size_high":0.01,"search_context_size_low":0.01,"search_context_size_medium":0.01},

View file

@ -4437,6 +4437,32 @@
"generateContent": true
}
},
"zerogpu": {
"display_name": "ZeroGPU (`zerogpu`)",
"url": "https://docs.litellm.ai/docs/providers/zerogpu",
"endpoints": {
"chat_completions": true,
"responses": true,
"text_completion": true,
"embeddings": false,
"image_generations": false,
"image_edits": false,
"audio_transcriptions": false,
"audio_speech": false,
"rerank": false,
"ocr": false,
"search": false,
"moderations": false,
"realtime": false,
"video_generations": false,
"batches": false,
"messages": true,
"interactions": true,
"systemone": false,
"a2a": true,
"generateContent": true
}
},
"ragflow": {
"display_name": "RAGFlow (`ragflow`)",
"url": "https://docs.litellm.ai/docs/providers/ragflow",

View file

@ -45,11 +45,14 @@ from litellm.caching.redis_cache import log_redis_failure
from litellm.constants import DYNAMIC_RATE_LIMIT_ERROR_THRESHOLD_PER_MINUTE, INTERNAL_CALL_ORIGIN_METADATA_KEY
from litellm.cost_calculator import get_usage_object
from litellm.integrations.custom_logger import CustomLogger
from litellm.litellm_core_utils.audio_utils.utils import estimate_audio_tokens
from litellm.litellm_core_utils.core_helpers import as_str_mapping
from litellm.litellm_core_utils.prompt_templates.common_utils import (
get_str_from_messages,
split_media_for_tpm,
)
from litellm.litellm_core_utils.ptu_pricing import is_ptu_cost_attribution_enabled
from litellm.litellm_core_utils.token_counter import offload_token_count
from litellm.litellm_core_utils.token_counter import estimate_image_tokens, offload_token_count
from litellm.llms.azure.ptu_capacity import normalized_tokens
from litellm.proxy._types import UserAPIKeyAuth
from litellm.proxy.auth.auth_utils import (
@ -98,6 +101,7 @@ if TYPE_CHECKING:
from opentelemetry.trace import Span as _Span
from litellm.proxy.utils import InternalUsageCache as _InternalUsageCache
from litellm.router import Router
from litellm.types.agents import AgentResponse
from litellm.types.caching import RedisPipelineIncrementOperation
@ -135,6 +139,18 @@ class RateLimitedModel:
return requested_limit if requested_limit is not None else limits.get(self.group)
def _resolve_media_models(router: "Router | None", model: str, team_id: str | None) -> tuple[str, ...]:
if router is None:
return (model,)
return tuple(dict.fromkeys(router.resolved_litellm_models(model, team_id, use_base_model=True))) or (model,)
def _resolve_media_models_via_proxy_router(model: str, team_id: str | None) -> tuple[str, ...]:
from litellm.proxy.proxy_server import llm_router
return _resolve_media_models(llm_router, model, team_id)
def _resolve_model_group_alias_via_proxy_router(model: str) -> str | None:
from litellm.proxy.proxy_server import llm_router
@ -862,6 +878,7 @@ class _PROXY_MaxParallelRequestsHandler_v3(CustomLogger):
time_provider: Callable[[], datetime] | None = None,
tag_rate_limit_resolver: TagRateLimitResolver = resolve_tag_rate_limits_from_db,
model_group_resolver: Callable[[str], str | None] = _resolve_model_group_alias_via_proxy_router,
media_model_resolver: Callable[[str, str | None], tuple[str, ...]] = _resolve_media_models_via_proxy_router,
ptu_team_ceiling_resolver: Callable[
[str, str], PTUTeamCeiling | None
] = _resolve_ptu_team_ceiling_via_proxy_router,
@ -872,6 +889,7 @@ class _PROXY_MaxParallelRequestsHandler_v3(CustomLogger):
self._time_provider = time_provider or datetime.now
self._tag_rate_limit_resolver = tag_rate_limit_resolver
self._model_group_resolver = model_group_resolver
self._media_model_resolver = media_model_resolver
self._ptu_team_ceiling_resolver = ptu_team_ceiling_resolver
self._fail_closed_resolver = fail_closed_resolver
self._force_hash_tag_grouping_resolver = force_hash_tag_grouping_resolver
@ -1133,11 +1151,12 @@ class _PROXY_MaxParallelRequestsHandler_v3(CustomLogger):
def _estimate_tokens_for_request(
self,
data: dict,
data: object,
model: str | None = None,
min_configured_tpm_limit: int | None = None,
call_type: str | None = None,
configured_output_tokens: int | None = None,
team_id: str | None = None,
) -> int:
"""
Estimate total tokens this request will consume so we can reserve them
@ -1164,6 +1183,8 @@ class _PROXY_MaxParallelRequestsHandler_v3(CustomLogger):
min_configured_tpm_limit=min_configured_tpm_limit,
call_type=call_type,
configured_output_tokens=configured_output_tokens,
team_id=team_id,
media_model_resolver=self._media_model_resolver,
)
total_estimated: Final = estimated_input_tokens + max_tokens_estimate
@ -1184,6 +1205,7 @@ class _PROXY_MaxParallelRequestsHandler_v3(CustomLogger):
call_type: str | None,
configured_output_tokens: int | None,
raw_estimate: int,
team_id: str | None = None,
) -> int:
"""The team PTU ceiling counts Azure normalized tokens, so its reservation weighs the
output budget the way the ceiling does instead of the raw sum the other scopes reserve."""
@ -1194,6 +1216,8 @@ class _PROXY_MaxParallelRequestsHandler_v3(CustomLogger):
min_configured_tpm_limit=min_configured_tpm_limit,
call_type=call_type,
configured_output_tokens=configured_output_tokens,
team_id=team_id,
media_model_resolver=self._media_model_resolver,
)
normalized: Final = normalized_tokens(
ceiling, prompt_tokens=estimated_input_tokens, completion_tokens=max_tokens_estimate
@ -1206,6 +1230,9 @@ class _PROXY_MaxParallelRequestsHandler_v3(CustomLogger):
min_configured_tpm_limit: int | None = None,
call_type: str | None = None,
configured_output_tokens: int | None = None,
include_media: bool = True,
team_id: str | None = None,
media_model_resolver: Callable[[str, str | None], tuple[str, ...]] | None = None,
) -> tuple[int, int]:
"""
Estimate input tokens and output (max_tokens) budget separately, so
@ -1229,10 +1256,12 @@ class _PROXY_MaxParallelRequestsHandler_v3(CustomLogger):
floor entirely, so the reservation reflects what this tenant's model
actually emits rather than one constant shared by every tenant.
"""
if not isinstance(data, dict):
request_data: Final = as_str_mapping(data)
if not isinstance(data, dict) or request_data is None:
return 0, 0
request_model: Final = request_data.get("model")
translated_data: Final = self._translate_google_genai_native_request(data, call_type)
estimable_data: Final = translated_data if translated_data is not None else data
estimable_data: Final = translated_data if translated_data is not None else request_data
selected_fields: Final[tuple[object | None, object | None, object | None]] = (
(None, None, estimable_data.get("input"))
if call_type in RESPONSES_API_CALL_TYPES or call_type in EMBEDDING_API_CALL_TYPES
@ -1249,10 +1278,27 @@ class _PROXY_MaxParallelRequestsHandler_v3(CustomLogger):
)
)
messages, prompt, raw_input = selected_fields
input_text: Final = (
original_input: Final = (
billable_input_since_newest_compaction(raw_input) if call_type in RESPONSES_API_CALL_TYPES else raw_input
)
media_source: Final = messages if messages is not None else original_input
projected, media = (
split_media_for_tpm(media_source)
if include_media and (isinstance(messages, list) or call_type in RESPONSES_API_CALL_TYPES)
else (original_input, ())
)
input_text: Final = projected if call_type in RESPONSES_API_CALL_TYPES else original_input
fallback_model: Final = request_model if isinstance(request_model, str) else None
resolved_models: Final = (
media_model_resolver(fallback_model or "", team_id)
if media and media_model_resolver is not None
else (fallback_model or "",)
)
media_tokens: Final = sum(
self._estimate_media_block_tokens(block, resolved_models, fallback_model) for block in media
)
total_chars: Final = (
len(get_str_from_messages(messages))
if isinstance(messages, list) and messages
@ -1267,7 +1313,9 @@ class _PROXY_MaxParallelRequestsHandler_v3(CustomLogger):
else 0
)
estimated_input_tokens: Final = max(1, total_chars // DEFAULT_CHARS_PER_TOKEN) if total_chars > 0 else 0
estimated_input_tokens: Final = (
max(1, total_chars // DEFAULT_CHARS_PER_TOKEN) if total_chars > 0 else 0
) + media_tokens
explicit_max_tokens: Final = self._get_explicit_output_cap(data, call_type)
generates_no_output: Final = (
@ -1283,7 +1331,7 @@ class _PROXY_MaxParallelRequestsHandler_v3(CustomLogger):
max_tokens_estimate: Final = (
0
if generates_no_output
or (explicit_max_tokens is None and total_chars == 0 and configured_output_tokens is None)
or (explicit_max_tokens is None and estimated_input_tokens == 0 and configured_output_tokens is None)
else explicit_max_tokens
if explicit_max_tokens is not None
else configured_output_tokens
@ -1293,6 +1341,24 @@ class _PROXY_MaxParallelRequestsHandler_v3(CustomLogger):
return estimated_input_tokens, max_tokens_estimate * self.get_output_candidate_count(data, call_type)
@staticmethod
def _estimate_media_block_tokens(
block: Mapping[str, object], models: tuple[str, ...], fallback_model: str | None
) -> int:
if block.get("type") == "input_audio":
audio: Final = _callback_kwargs_mapping(block.get("input_audio"))
data: Final = audio.get("data")
return estimate_audio_tokens(data if isinstance(data, str) else None, models, fallback_model=fallback_model)
image: Final = _callback_kwargs_mapping(block.get("image_url"))
url: Final = image.get("url")
detail: Final = image.get("detail")
return estimate_image_tokens(
models,
url if isinstance(url, str) else "",
detail if isinstance(detail, str) else "auto",
fallback_model=fallback_model,
)
def _is_redis_cluster(self) -> bool:
"""
Check if the dual cache is using Redis cluster.
@ -3958,7 +4024,9 @@ class _PROXY_MaxParallelRequestsHandler_v3(CustomLogger):
except Exception: # noqa: BLE001 # tokenizer failures degrade to the cheap estimate
if call_type in RERANK_API_CALL_TYPES and isinstance(selected_text, str):
return max(0, len(selected_text) // DEFAULT_CHARS_PER_TOKEN)
estimated_input_tokens, _ = self._estimate_input_and_output_tokens(data=data, call_type=call_type)
estimated_input_tokens, _ = self._estimate_input_and_output_tokens(
data=data, call_type=call_type, include_media=False
)
return estimated_input_tokens + audio_token_estimate
async def _reserve_project_io_tokens_or_raise(
@ -4003,6 +4071,7 @@ class _PROXY_MaxParallelRequestsHandler_v3(CustomLogger):
data=data,
min_configured_tpm_limit=min_configured_otpm_limit,
call_type=call_type,
include_media=False,
)
raw_estimated_input_tokens: Final = await offload_token_count(self._estimate_precise_input_tokens)(
data=data, model=requested_model, call_type=call_type
@ -4360,12 +4429,13 @@ class _PROXY_MaxParallelRequestsHandler_v3(CustomLogger):
# Post-call reconciliation refunds the over-reservation
# delta when actual usage comes in below the floor.
estimated_tokens: Final = max(
self._estimate_tokens_for_request(
await offload_token_count(self._estimate_tokens_for_request)(
data=data,
model=requested_model,
min_configured_tpm_limit=min_configured_tpm_limit,
call_type=call_type,
configured_output_tokens=configured_output_tokens,
team_id=user_api_key_dict.team_id,
),
1,
)
@ -4383,13 +4453,14 @@ class _PROXY_MaxParallelRequestsHandler_v3(CustomLogger):
)
capped_request: Final = _REQUEST_RATE_LIMIT_DATA.validate_python(data)
ptu_estimated_tokens: Final = self._estimate_ptu_tokens_for_request(
ptu_estimated_tokens: Final = await offload_token_count(self._estimate_ptu_tokens_for_request)(
ceiling=stash.ptu_ceiling,
data=capped_request,
min_configured_tpm_limit=min_configured_tpm_limit,
call_type=call_type,
configured_output_tokens=configured_output_tokens,
raw_estimate=estimated_tokens,
team_id=user_api_key_dict.team_id,
)
if stash.ptu_ceiling is not None and ptu_estimated_tokens > stash.ptu_ceiling.tpm_limit:
if not reuses_request_slot:

View file

@ -4145,6 +4145,34 @@
],
"default_model_placeholder": "gpt-3.5-turbo"
},
{
"provider": "ZeroGPU",
"provider_display_name": "ZeroGPU",
"litellm_provider": "zerogpu",
"credential_fields": [
{
"key": "api_base",
"label": "API Base",
"placeholder": "https://api.zerogpu.ai/v1",
"tooltip": null,
"required": false,
"field_type": "text",
"options": null,
"default_value": null
},
{
"key": "api_key",
"label": "API Key",
"placeholder": null,
"tooltip": null,
"required": true,
"field_type": "password",
"options": null,
"default_value": null
}
],
"default_model_placeholder": "zerogpu/gpt-oss-120b"
},
{
"provider": "CURSOR",
"provider_display_name": "Cursor",

View file

@ -1,8 +1,10 @@
import base64
import os
import re
from collections.abc import Callable, Collection, Iterable, Mapping, Sequence
from functools import reduce
from typing import Any, Final, Optional, TypeVar, Union, cast, get_type_hints, overload
from types import MappingProxyType
from typing import Any, Final, Optional, TypeVar, Union, cast, get_args, get_type_hints, overload
from pydantic import BaseModel, TypeAdapter
from typing_extensions import TypedDict as ExtraItemsTypedDict
@ -14,6 +16,7 @@ from litellm.litellm_core_utils.core_helpers import as_str_mapping, attribute_or
from litellm.litellm_core_utils.dot_notation_indexing import delete_nested_value, is_nested_path
from litellm.litellm_core_utils.get_provider_specific_headers import ProviderSpecificHeaderUtils
from litellm.llms.base_llm.responses.transformation import BaseResponsesAPIConfig, ReasoningSummaryLevel
from litellm.llms.openai.chat.gpt_transformation import OpenAIGPTConfig
from litellm.types.llms.openai import (
AllMessageValues,
InputTokensDetails,
@ -29,6 +32,7 @@ from litellm.types.responses.main import DecodedResponseId
from litellm.types.utils import (
CacheCreationTokenDetails,
CompletionTokensDetailsWrapper,
ModelInfo,
PromptTokensDetailsWrapper,
ProviderSpecificHeader,
SpecialEnums,
@ -1382,3 +1386,98 @@ def auto_reasoning_summary(model: str, custom_llm_provider: object) -> Reasoning
provider, model.removeprefix(f"{provider}/")
)
return "detailed" if responses_config is None else responses_config.auto_reasoning_summary()
def opt_in_auto_reasoning_summary(model: str | None, custom_llm_provider: object) -> ReasoningSummaryLevel | None:
if not (litellm.reasoning_auto_summary or os.getenv("LITELLM_REASONING_AUTO_SUMMARY", "false").lower() == "true"):
return None
return "detailed" if model is None else auto_reasoning_summary(model, custom_llm_provider)
def reasoning_with_summary_default(
reasoning: Mapping[str, object], summary_alias: object, model: str, custom_llm_provider: object
) -> Mapping[str, object]:
if "summary" in reasoning:
return {**reasoning}
summary: Final = (
summary_alias if summary_alias is not None else opt_in_auto_reasoning_summary(model, custom_llm_provider)
)
return {**reasoning} if summary is None else {**reasoning, "summary": summary}
_SUMMARY_VALUES: Final = frozenset((*get_args(ReasoningSummaryLevel), None))
def _openai_model_info(model: str) -> ModelInfo | None:
from litellm.utils import ModelInfoLookupMiss, lookup_model_info_for_registration
model_info: Final = lookup_model_info_for_registration(model.removeprefix("responses/"), "openai")
return None if isinstance(model_info, ModelInfoLookupMiss) else model_info
def _declared_values(values: Sequence[str] | None) -> frozenset[str | None] | None:
return None if values is None else frozenset(values)
def _openai_reasoning_field_values(model: str) -> Mapping[str, frozenset[str | None] | None]:
model_info: Final = _openai_model_info(model)
if model_info is None:
return MappingProxyType({"summary": _SUMMARY_VALUES, "mode": None, "context": None})
if not model_info.get("supports_reasoning"):
return MappingProxyType({})
return MappingProxyType(
{
"summary": _SUMMARY_VALUES,
"mode": _declared_values(model_info.get("reasoning_modes")),
"context": _declared_values(model_info.get("reasoning_contexts")),
}
)
def _openai_accepts_reasoning_field(
key: str, value: object, values_by_field: Mapping[str, frozenset[str | None] | None]
) -> bool:
if key == "effort":
return True
if key not in values_by_field:
return False
allowed: Final = values_by_field[key]
return allowed is None or (isinstance(value, str | None) and value in allowed)
def _openai_accepted_reasoning(reasoning: Mapping[str, object], model: str) -> Mapping[str, object]:
values_by_field: Final = _openai_reasoning_field_values(model)
return {
key: value for key, value in reasoning.items() if _openai_accepts_reasoning_field(key, value, values_by_field)
}
def _additional_drop_paths(additional_drop_params: object) -> tuple[str, ...]:
if additional_drop_params.__class__ is not list:
return ()
return tuple(path for path in iter_items(additional_drop_params) if isinstance(path, str))
def openai_bridge_reasoning(
*,
requested: Mapping[str, object],
effort_kept: bool,
summary_alias: object,
model: str,
drop_params: bool,
additional_drop_params: object,
) -> Mapping[str, object] | None:
drop_paths: Final = _additional_drop_paths(additional_drop_params)
if "reasoning_effort" in drop_paths:
return None
kept: Final = requested if effort_kept else {key: value for key, value in requested.items() if key != "effort"}
with_summary: Final = reasoning_with_summary_default(kept, summary_alias, model, "openai")
accepted: Final = (
_openai_accepted_reasoning(with_summary, model)
if drop_params and OpenAIGPTConfig.is_openai_catalog_model(model)
else {**with_summary}
)
remaining: Final = as_str_mapping(
_apply_nested_drop_params({"reasoning_effort": accepted}, list(drop_paths)).get("reasoning_effort")
)
return remaining or None

View file

@ -10365,28 +10365,38 @@ class Router:
model_info[field] = backend_value
@staticmethod
def _cost_map_backend_model(deployment: Deployment) -> str:
model_info_base_model: Final = deployment.model_info.base_model
def _cost_map_backend_model(deployment: Deployment | DeploymentTypedDict) -> str:
model_info_base_model: Final = (
deployment.model_info.base_model
if isinstance(deployment, Deployment)
else _configured_model_info(deployment).get("base_model")
)
if isinstance(model_info_base_model, str) and model_info_base_model:
return model_info_base_model
params_base_model: Final = deployment.litellm_params.get("base_model")
params: Final = _LITELLM_PARAMS_ADAPTER.validate_python(
deployment.litellm_params.model_dump(exclude_none=True)
if isinstance(deployment, Deployment)
else deployment["litellm_params"]
)
params_base_model: Final = params.get("base_model")
if isinstance(params_base_model, str) and params_base_model:
return params_base_model
params: Final = _LITELLM_PARAMS_ADAPTER.validate_python(deployment.litellm_params.model_dump(exclude_none=True))
model: Final = (
deployment.litellm_params.model
if isinstance(deployment, Deployment)
else deployment["litellm_params"].get("model", "")
)
invoked_model: Final = params.get("model_id")
if (
isinstance(invoked_model, str)
and invoked_model
and (
deployment.litellm_params.custom_llm_provider == "bedrock"
or deployment.litellm_params.model.startswith("bedrock/")
)
and (params.get("custom_llm_provider") == "bedrock" or model.startswith("bedrock/"))
):
profile_model: Final = invoked_model if invoked_model.startswith("bedrock/") else f"bedrock/{invoked_model}"
profile_info: Final = lookup_model_info_for_registration(model=profile_model, custom_llm_provider="bedrock")
if not isinstance(profile_info, ModelInfoLookupMiss):
return profile_model
return deployment.litellm_params.model
return model
@staticmethod
def _inherit_builtin_service_tier_pricing(
@ -13946,7 +13956,9 @@ class Router:
]
return named or self._get_wildcard_deployments(model_name=model_group)
def resolved_litellm_models(self, model_name: str, team_id: str | None = None) -> tuple[str, ...]:
def resolved_litellm_models(
self, model_name: str, team_id: str | None = None, *, use_base_model: bool = False
) -> tuple[str, ...]:
"""The provider model strings `model_name` can actually be served by on this proxy.
`get_model_list` composes every channel the request path itself uses (exact name,
@ -13959,6 +13971,12 @@ class Router:
deployment behind it - so the fallback for an empty result is the caller's policy,
never this function's.
"""
if use_base_model:
return tuple(
backend_model
for deployment in self.get_model_list(model_name=model_name, team_id=team_id) or ()
if (backend_model := self._cost_map_backend_model(deployment))
)
return tuple(
litellm_model
for deployment in self.get_model_list(model_name=model_name, team_id=team_id) or ()

View file

@ -225,6 +225,8 @@ class ProviderSpecificModelInfo(TypedDict, total=False):
supports_xhigh_reasoning_effort: bool | None
supports_max_reasoning_effort: bool | None
reasoning_effort_levels: ReadOnly[Sequence[str] | None]
reasoning_modes: ReadOnly[Sequence[str] | None]
reasoning_contexts: ReadOnly[Sequence[str] | None]
default_reasoning_effort: ReadOnly[Literal["none", "minimal", "low", "medium", "high", "xhigh"] | None]
supports_output_config: bool | None
supports_image_size: bool | None
@ -4587,6 +4589,7 @@ class LlmProviders(str, Enum):
PRISM = "prism"
OPENINFER = "openinfer"
INFERENCEAPIS = "inferenceapis"
ZEROGPU = "zerogpu"
DARKBLOOM = "darkbloom"
META = "meta"
SAIL = "sail"

View file

@ -7000,6 +7000,8 @@ def get_model_info_helper(
supports_xhigh_reasoning_effort=_model_info.get("supports_xhigh_reasoning_effort", None),
supports_max_reasoning_effort=_model_info.get("supports_max_reasoning_effort", None),
reasoning_effort_levels=_model_info.get("reasoning_effort_levels", None),
reasoning_modes=_model_info.get("reasoning_modes", None),
reasoning_contexts=_model_info.get("reasoning_contexts", None),
default_reasoning_effort=_model_info.get("default_reasoning_effort", None),
bedrock_output_config_effort_ceiling=_model_info.get("bedrock_output_config_effort_ceiling", None),
bedrock_converse_supports_strict_tools=_model_info.get("bedrock_converse_supports_strict_tools", None),

View file

@ -35598,6 +35598,13 @@
"output_cost_per_token_batches": 5e-06,
"output_cost_per_token_flex": 5e-06,
"output_cost_per_token_priority": 2e-05,
"reasoning_contexts": [
"auto",
"current_turn"
],
"reasoning_modes": [
"standard"
],
"search_context_cost_per_query": {
"search_context_size_high": 0.01,
"search_context_size_low": 0.01,
@ -35675,6 +35682,13 @@
"mode": "chat",
"output_cost_per_token": 1e-05,
"output_cost_per_token_priority": 2e-05,
"reasoning_contexts": [
"auto",
"current_turn"
],
"reasoning_modes": [
"standard"
],
"search_context_cost_per_query": {
"search_context_size_high": 0.01,
"search_context_size_low": 0.01,
@ -35851,6 +35865,13 @@
"mode": "chat",
"output_cost_per_token": 1e-05,
"output_cost_per_token_priority": 2e-05,
"reasoning_contexts": [
"auto",
"current_turn"
],
"reasoning_modes": [
"standard"
],
"search_context_cost_per_query": {
"search_context_size_high": 0.01,
"search_context_size_low": 0.01,
@ -35902,6 +35923,13 @@
"mode": "chat",
"output_cost_per_token": 1.4e-05,
"output_cost_per_token_priority": 2.8e-05,
"reasoning_contexts": [
"auto",
"current_turn"
],
"reasoning_modes": [
"standard"
],
"search_context_cost_per_query": {
"search_context_size_high": 0.01,
"search_context_size_low": 0.01,
@ -36017,6 +36045,13 @@
"mode": "chat",
"output_cost_per_token": 1.4e-05,
"output_cost_per_token_priority": 2.8e-05,
"reasoning_contexts": [
"auto",
"current_turn"
],
"reasoning_modes": [
"standard"
],
"search_context_cost_per_query": {
"search_context_size_high": 0.01,
"search_context_size_low": 0.01,
@ -36066,6 +36101,13 @@
"mode": "responses",
"output_cost_per_token": 0.000168,
"output_cost_per_token_batches": 8.4e-05,
"reasoning_contexts": [
"auto",
"current_turn"
],
"reasoning_modes": [
"pro"
],
"search_context_cost_per_query": {
"search_context_size_high": 0.01,
"search_context_size_low": 0.01,
@ -36107,6 +36149,13 @@
"mode": "responses",
"output_cost_per_token": 0.000168,
"output_cost_per_token_batches": 8.4e-05,
"reasoning_contexts": [
"auto",
"current_turn"
],
"reasoning_modes": [
"pro"
],
"search_context_cost_per_query": {
"search_context_size_high": 0.01,
"search_context_size_low": 0.01,
@ -36194,6 +36243,15 @@
"output_cost_per_token_flex": 2.5e-05,
"output_cost_per_token_priority": 0.0001,
"output_cost_per_token_ultrafast": 0.0003,
"reasoning_contexts": [
"auto",
"current_turn",
"all_turns"
],
"reasoning_modes": [
"standard",
"pro"
],
"regional_processing_uplift_multiplier_eu": 1.1,
"regional_processing_uplift_multiplier_us": 1.1,
"search_context_cost_per_query": {
@ -36278,6 +36336,15 @@
"output_cost_per_token_above_272k_tokens_batches": 7.5e-06,
"output_cost_per_token_flex": 5e-06,
"output_cost_per_token_priority": 2e-05,
"reasoning_contexts": [
"auto",
"current_turn",
"all_turns"
],
"reasoning_modes": [
"standard",
"pro"
],
"regional_processing_uplift_multiplier_eu": 1.1,
"regional_processing_uplift_multiplier_us": 1.1,
"search_context_cost_per_query": {
@ -36370,6 +36437,15 @@
"output_cost_per_token_above_272k_tokens_batches": 3.75e-07,
"output_cost_per_token_flex": 2.5e-07,
"output_cost_per_token_priority": 1e-06,
"reasoning_contexts": [
"auto",
"current_turn",
"all_turns"
],
"reasoning_modes": [
"standard",
"pro"
],
"regional_processing_uplift_multiplier_eu": 1.1,
"regional_processing_uplift_multiplier_us": 1.1,
"search_context_cost_per_query": {
@ -36453,6 +36529,15 @@
"output_cost_per_token_above_272k_tokens_batches": 1.5e-05,
"output_cost_per_token_flex": 1e-05,
"output_cost_per_token_priority": 4e-05,
"reasoning_contexts": [
"auto",
"current_turn",
"all_turns"
],
"reasoning_modes": [
"standard",
"pro"
],
"regional_processing_uplift_multiplier_eu": 1.1,
"regional_processing_uplift_multiplier_us": 1.1,
"search_context_cost_per_query": {
@ -36534,6 +36619,15 @@
"output_cost_per_token_above_272k_tokens_batches": 1.5e-05,
"output_cost_per_token_flex": 1e-05,
"output_cost_per_token_priority": 4e-05,
"reasoning_contexts": [
"auto",
"current_turn",
"all_turns"
],
"reasoning_modes": [
"standard",
"pro"
],
"regional_processing_uplift_multiplier_eu": 1.1,
"regional_processing_uplift_multiplier_us": 1.1,
"search_context_cost_per_query": {
@ -36617,6 +36711,15 @@
"output_cost_per_token_above_272k_tokens_batches": 9e-06,
"output_cost_per_token_flex": 6e-06,
"output_cost_per_token_priority": 2.4e-05,
"reasoning_contexts": [
"auto",
"current_turn",
"all_turns"
],
"reasoning_modes": [
"standard",
"pro"
],
"regional_processing_uplift_multiplier_eu": 1.1,
"regional_processing_uplift_multiplier_us": 1.1,
"search_context_cost_per_query": {
@ -36699,6 +36802,15 @@
"output_cost_per_token_above_272k_tokens_batches": 9e-07,
"output_cost_per_token_flex": 6e-07,
"output_cost_per_token_priority": 2.4e-06,
"reasoning_contexts": [
"auto",
"current_turn",
"all_turns"
],
"reasoning_modes": [
"standard",
"pro"
],
"regional_processing_uplift_multiplier_eu": 1.1,
"regional_processing_uplift_multiplier_us": 1.1,
"search_context_cost_per_query": {
@ -36936,6 +37048,14 @@
"reasoning_effort_levels": [
"medium"
],
"reasoning_contexts": [
"auto",
"current_turn",
"all_turns"
],
"reasoning_modes": [
"standard"
],
"source": "https://developers.openai.com/api/docs/pricing",
"supported_endpoints": [
"/v1/chat/completions",
@ -36983,6 +37103,14 @@
"output_cost_per_token_batches": 1.5e-05,
"output_cost_per_token_above_272k_tokens_batches": 2.25e-05,
"output_cost_per_token_priority": 7.5e-05,
"reasoning_contexts": [
"auto",
"current_turn",
"all_turns"
],
"reasoning_modes": [
"standard"
],
"regional_processing_uplift_multiplier_eu": 1.1,
"regional_processing_uplift_multiplier_us": 1.1,
"search_context_cost_per_query": {
@ -37045,6 +37173,14 @@
"output_cost_per_token_batches": 1.5e-05,
"output_cost_per_token_above_272k_tokens_batches": 2.25e-05,
"output_cost_per_token_priority": 7.5e-05,
"reasoning_contexts": [
"auto",
"current_turn",
"all_turns"
],
"reasoning_modes": [
"standard"
],
"regional_processing_uplift_multiplier_eu": 1.1,
"regional_processing_uplift_multiplier_us": 1.1,
"search_context_cost_per_query": {
@ -37099,6 +37235,14 @@
"output_cost_per_token_flex": 9e-05,
"output_cost_per_token_batches": 9e-05,
"output_cost_per_token_above_272k_tokens_batches": 0.000135,
"reasoning_contexts": [
"auto",
"current_turn",
"all_turns"
],
"reasoning_modes": [
"pro"
],
"regional_processing_uplift_multiplier_eu": 1.1,
"regional_processing_uplift_multiplier_us": 1.1,
"search_context_cost_per_query": {
@ -37150,6 +37294,14 @@
"output_cost_per_token_flex": 9e-05,
"output_cost_per_token_batches": 9e-05,
"output_cost_per_token_above_272k_tokens_batches": 0.000135,
"reasoning_contexts": [
"auto",
"current_turn",
"all_turns"
],
"reasoning_modes": [
"pro"
],
"regional_processing_uplift_multiplier_eu": 1.1,
"regional_processing_uplift_multiplier_us": 1.1,
"search_context_cost_per_query": {
@ -37209,6 +37361,14 @@
"output_cost_per_token_batches": 7.5e-06,
"output_cost_per_token_above_272k_tokens_batches": 1.125e-05,
"output_cost_per_token_priority": 3e-05,
"reasoning_contexts": [
"auto",
"current_turn",
"all_turns"
],
"reasoning_modes": [
"standard"
],
"regional_processing_uplift_multiplier_eu": 1.1,
"regional_processing_uplift_multiplier_us": 1.1,
"supported_endpoints": [
@ -37266,6 +37426,14 @@
"output_cost_per_token_batches": 7.5e-06,
"output_cost_per_token_above_272k_tokens_batches": 1.125e-05,
"output_cost_per_token_priority": 3e-05,
"reasoning_contexts": [
"auto",
"current_turn",
"all_turns"
],
"reasoning_modes": [
"standard"
],
"regional_processing_uplift_multiplier_eu": 1.1,
"regional_processing_uplift_multiplier_us": 1.1,
"supported_endpoints": [
@ -37315,6 +37483,14 @@
"output_cost_per_token_flex": 9e-05,
"output_cost_per_token_batches": 9e-05,
"output_cost_per_token_above_272k_tokens_batches": 0.000135,
"reasoning_contexts": [
"auto",
"current_turn",
"all_turns"
],
"reasoning_modes": [
"pro"
],
"regional_processing_uplift_multiplier_eu": 1.1,
"regional_processing_uplift_multiplier_us": 1.1,
"search_context_cost_per_query": {
@ -37367,6 +37543,14 @@
"output_cost_per_token_flex": 9e-05,
"output_cost_per_token_batches": 9e-05,
"output_cost_per_token_above_272k_tokens_batches": 0.000135,
"reasoning_contexts": [
"auto",
"current_turn",
"all_turns"
],
"reasoning_modes": [
"pro"
],
"regional_processing_uplift_multiplier_eu": 1.1,
"regional_processing_uplift_multiplier_us": 1.1,
"search_context_cost_per_query": {
@ -37421,6 +37605,14 @@
"output_cost_per_token_flex": 2.25e-06,
"output_cost_per_token_batches": 2.25e-06,
"output_cost_per_token_priority": 9e-06,
"reasoning_contexts": [
"auto",
"current_turn",
"all_turns"
],
"reasoning_modes": [
"standard"
],
"regional_processing_uplift_multiplier_eu": 1.1,
"regional_processing_uplift_multiplier_us": 1.1,
"search_context_cost_per_query": {
@ -37475,6 +37667,14 @@
"output_cost_per_token_flex": 2.25e-06,
"output_cost_per_token_batches": 2.25e-06,
"output_cost_per_token_priority": 9e-06,
"reasoning_contexts": [
"auto",
"current_turn",
"all_turns"
],
"reasoning_modes": [
"standard"
],
"regional_processing_uplift_multiplier_eu": 1.1,
"regional_processing_uplift_multiplier_us": 1.1,
"search_context_cost_per_query": {
@ -37526,6 +37726,14 @@
"output_cost_per_token": 1.25e-06,
"output_cost_per_token_flex": 6.25e-07,
"output_cost_per_token_batches": 6.25e-07,
"reasoning_contexts": [
"auto",
"current_turn",
"all_turns"
],
"reasoning_modes": [
"standard"
],
"regional_processing_uplift_multiplier_eu": 1.1,
"regional_processing_uplift_multiplier_us": 1.1,
"search_context_cost_per_query": {
@ -37578,6 +37786,14 @@
"output_cost_per_token": 1.25e-06,
"output_cost_per_token_flex": 6.25e-07,
"output_cost_per_token_batches": 6.25e-07,
"reasoning_contexts": [
"auto",
"current_turn",
"all_turns"
],
"reasoning_modes": [
"standard"
],
"regional_processing_uplift_multiplier_eu": 1.1,
"regional_processing_uplift_multiplier_us": 1.1,
"search_context_cost_per_query": {
@ -37624,6 +37840,13 @@
"mode": "responses",
"output_cost_per_token": 0.00012,
"output_cost_per_token_batches": 6e-05,
"reasoning_contexts": [
"auto",
"current_turn"
],
"reasoning_modes": [
"pro"
],
"search_context_cost_per_query": {
"search_context_size_high": 0.01,
"search_context_size_low": 0.01,
@ -37666,6 +37889,13 @@
"mode": "responses",
"output_cost_per_token": 0.00012,
"output_cost_per_token_batches": 6e-05,
"reasoning_contexts": [
"auto",
"current_turn"
],
"reasoning_modes": [
"pro"
],
"search_context_cost_per_query": {
"search_context_size_high": 0.01,
"search_context_size_low": 0.01,
@ -37716,6 +37946,13 @@
"output_cost_per_token_batches": 5e-06,
"output_cost_per_token_flex": 5e-06,
"output_cost_per_token_priority": 2e-05,
"reasoning_contexts": [
"auto",
"current_turn"
],
"reasoning_modes": [
"standard"
],
"search_context_cost_per_query": {
"search_context_size_high": 0.01,
"search_context_size_low": 0.01,
@ -37826,6 +38063,13 @@
"mode": "responses",
"output_cost_per_token": 1.4e-05,
"output_cost_per_token_priority": 2.8e-05,
"reasoning_contexts": [
"auto",
"current_turn"
],
"reasoning_modes": [
"standard"
],
"search_context_cost_per_query": {
"search_context_size_high": 0.01,
"search_context_size_low": 0.01,
@ -37908,6 +38152,13 @@
"output_cost_per_token_batches": 1e-06,
"output_cost_per_token_flex": 1e-06,
"output_cost_per_token_priority": 3.6e-06,
"reasoning_contexts": [
"auto",
"current_turn"
],
"reasoning_modes": [
"standard"
],
"search_context_cost_per_query": {
"search_context_size_high": 0.01,
"search_context_size_low": 0.01,
@ -37959,6 +38210,13 @@
"output_cost_per_token_batches": 1e-06,
"output_cost_per_token_flex": 1e-06,
"output_cost_per_token_priority": 3.6e-06,
"reasoning_contexts": [
"auto",
"current_turn"
],
"reasoning_modes": [
"standard"
],
"search_context_cost_per_query": {
"search_context_size_high": 0.01,
"search_context_size_low": 0.01,
@ -38007,6 +38265,13 @@
"output_cost_per_token": 4e-07,
"output_cost_per_token_batches": 2e-07,
"output_cost_per_token_flex": 2e-07,
"reasoning_contexts": [
"auto",
"current_turn"
],
"reasoning_modes": [
"standard"
],
"search_context_cost_per_query": {
"search_context_size_high": 0.01,
"search_context_size_low": 0.01,
@ -38055,6 +38320,13 @@
"output_cost_per_token": 4e-07,
"output_cost_per_token_batches": 2e-07,
"output_cost_per_token_flex": 2e-07,
"reasoning_contexts": [
"auto",
"current_turn"
],
"reasoning_modes": [
"standard"
],
"search_context_cost_per_query": {
"search_context_size_high": 0.01,
"search_context_size_low": 0.01,
@ -43821,6 +44093,13 @@
"output_cost_per_token_batches": 4e-06,
"output_cost_per_token_flex": 4e-06,
"output_cost_per_token_priority": 1.4e-05,
"reasoning_contexts": [
"auto",
"current_turn"
],
"reasoning_modes": [
"standard"
],
"search_context_cost_per_query": {
"search_context_size_high": 0.01,
"search_context_size_low": 0.01,
@ -43929,6 +44208,13 @@
"output_cost_per_token_batches": 4e-06,
"output_cost_per_token_flex": 4e-06,
"output_cost_per_token_priority": 1.4e-05,
"reasoning_contexts": [
"auto",
"current_turn"
],
"reasoning_modes": [
"standard"
],
"search_context_cost_per_query": {
"search_context_size_high": 0.01,
"search_context_size_low": 0.01,
@ -44008,6 +44294,13 @@
"mode": "responses",
"output_cost_per_token": 8e-05,
"output_cost_per_token_batches": 4e-05,
"reasoning_contexts": [
"auto",
"current_turn"
],
"reasoning_modes": [
"pro"
],
"search_context_cost_per_query": {
"search_context_size_high": 0.01,
"search_context_size_low": 0.01,
@ -44045,6 +44338,13 @@
"mode": "responses",
"output_cost_per_token": 8e-05,
"output_cost_per_token_batches": 4e-05,
"reasoning_contexts": [
"auto",
"current_turn"
],
"reasoning_modes": [
"pro"
],
"search_context_cost_per_query": {
"search_context_size_high": 0.01,
"search_context_size_low": 0.01,
@ -65863,6 +66163,180 @@
"supports_reasoning": true,
"supports_tool_choice": true
},
"zerogpu/deepseek-v4.1-flash": {
"cache_read_input_token_cost": 3e-09,
"input_cost_per_token": 3e-07,
"litellm_provider": "zerogpu",
"max_input_tokens": 1048576,
"max_output_tokens": 65536,
"mode": "chat",
"output_cost_per_token": 1.2e-06,
"source": "https://docs.zerogpu.ai/docs/model-catalog",
"supports_function_calling": true,
"supports_prompt_caching": true,
"supports_reasoning": true,
"supports_response_schema": true,
"supports_tool_choice": true
},
"zerogpu/glm-5.3-flash": {
"cache_read_input_token_cost": 1.5e-08,
"input_cost_per_token": 1e-07,
"litellm_provider": "zerogpu",
"max_input_tokens": 1048576,
"max_output_tokens": 32768,
"mode": "chat",
"output_cost_per_token": 3.5e-07,
"source": "https://docs.zerogpu.ai/docs/model-catalog",
"supports_function_calling": true,
"supports_prompt_caching": true,
"supports_reasoning": true,
"supports_response_schema": true,
"supports_tool_choice": true
},
"zerogpu/gpt-4.1-mini": {
"cache_read_input_token_cost": 1e-07,
"input_cost_per_token": 4e-07,
"litellm_provider": "zerogpu",
"max_input_tokens": 1047576,
"max_output_tokens": 32768,
"mode": "chat",
"output_cost_per_token": 1.6e-06,
"source": "https://docs.zerogpu.ai/docs/model-catalog",
"supports_function_calling": true,
"supports_prompt_caching": true,
"supports_response_schema": true,
"supports_tool_choice": true
},
"zerogpu/gpt-5.4-nano": {
"cache_read_input_token_cost": 2e-08,
"input_cost_per_token": 2e-07,
"litellm_provider": "zerogpu",
"max_input_tokens": 272000,
"max_output_tokens": 128000,
"mode": "chat",
"output_cost_per_token": 1.25e-06,
"source": "https://docs.zerogpu.ai/docs/model-catalog",
"supports_function_calling": true,
"supports_prompt_caching": true,
"supports_reasoning": true,
"supports_response_schema": true,
"supports_tool_choice": true
},
"zerogpu/gpt-5.6-luna": {
"cache_read_input_token_cost": 2e-07,
"input_cost_per_token": 2e-07,
"litellm_provider": "zerogpu",
"max_input_tokens": 272000,
"max_output_tokens": 128000,
"mode": "chat",
"output_cost_per_token": 1.2e-06,
"source": "https://docs.zerogpu.ai/docs/model-catalog",
"supports_function_calling": true,
"supports_prompt_caching": true,
"supports_reasoning": true,
"supports_response_schema": true,
"supports_tool_choice": true
},
"zerogpu/gpt-6-luna": {
"cache_read_input_token_cost": 1e-08,
"input_cost_per_token": 1e-07,
"litellm_provider": "zerogpu",
"max_input_tokens": 272000,
"max_output_tokens": 128000,
"mode": "chat",
"output_cost_per_token": 5e-07,
"source": "https://docs.zerogpu.ai/docs/model-catalog",
"supports_function_calling": true,
"supports_prompt_caching": true,
"supports_reasoning": true,
"supports_response_schema": true,
"supports_tool_choice": true,
"supports_vision": true
},
"zerogpu/gpt-oss-120b": {
"cache_read_input_token_cost": 3e-08,
"input_cost_per_token": 1.5e-07,
"litellm_provider": "zerogpu",
"max_input_tokens": 128000,
"max_output_tokens": 32768,
"mode": "chat",
"output_cost_per_token": 6e-07,
"source": "https://docs.zerogpu.ai/docs/model-catalog",
"supports_function_calling": true,
"supports_reasoning": true,
"supports_response_schema": true,
"supports_tool_choice": true
},
"zerogpu/LFM2.5-1.2B-Instruct": {
"input_cost_per_token": 2e-08,
"litellm_provider": "zerogpu",
"max_input_tokens": 32768,
"max_output_tokens": 8192,
"mode": "chat",
"output_cost_per_token": 5e-08,
"source": "https://docs.zerogpu.ai/docs/model-catalog"
},
"zerogpu/LFM2.5-1.2B-Thinking": {
"input_cost_per_token": 2e-08,
"litellm_provider": "zerogpu",
"max_input_tokens": 32768,
"max_output_tokens": 8192,
"mode": "chat",
"output_cost_per_token": 5e-08,
"source": "https://docs.zerogpu.ai/docs/model-catalog"
},
"zerogpu/llama-3.1-8b-instruct-fast": {
"cache_read_input_token_cost": 2.5e-08,
"input_cost_per_token": 1.5e-07,
"litellm_provider": "zerogpu",
"max_input_tokens": 131072,
"max_output_tokens": 8192,
"mode": "chat",
"output_cost_per_token": 2.8e-07,
"source": "https://docs.zerogpu.ai/docs/model-catalog",
"supports_function_calling": true,
"supports_response_schema": true,
"supports_tool_choice": true
},
"zerogpu/llama-3.2-3b-instruct-fast": {
"cache_read_input_token_cost": 2.5e-08,
"input_cost_per_token": 5e-08,
"litellm_provider": "zerogpu",
"max_input_tokens": 131072,
"max_output_tokens": 8192,
"mode": "chat",
"output_cost_per_token": 3e-07,
"source": "https://docs.zerogpu.ai/docs/model-catalog"
},
"zerogpu/qwen3-30b-a3b-fp8": {
"cache_read_input_token_cost": 5e-08,
"input_cost_per_token": 1e-07,
"litellm_provider": "zerogpu",
"max_input_tokens": 32768,
"max_output_tokens": 16384,
"mode": "chat",
"output_cost_per_token": 4.5e-07,
"source": "https://docs.zerogpu.ai/docs/model-catalog",
"supports_function_calling": true,
"supports_reasoning": true,
"supports_response_schema": true,
"supports_tool_choice": true
},
"zerogpu/qwen3.6-35b-a3b": {
"cache_read_input_token_cost": 2e-07,
"input_cost_per_token": 2e-07,
"litellm_provider": "zerogpu",
"max_input_tokens": 262144,
"max_output_tokens": 16384,
"mode": "chat",
"output_cost_per_token": 1.5e-06,
"source": "https://docs.zerogpu.ai/docs/model-catalog",
"supports_function_calling": true,
"supports_reasoning": true,
"supports_response_schema": true,
"supports_tool_choice": true,
"supports_vision": true
},
"together_ai/Qwen/Qwen3.8-Flash": {
"input_cost_per_token": 1.5e-07,
"litellm_provider": "together_ai",
@ -73288,6 +73762,15 @@
"output_cost_per_token_flex": 5e-06,
"output_cost_per_token_priority": 2e-05,
"output_cost_per_token_ultrafast": 6e-05,
"reasoning_contexts": [
"auto",
"current_turn",
"all_turns"
],
"reasoning_modes": [
"standard",
"pro"
],
"regional_processing_uplift_multiplier_eu": 1.1,
"regional_processing_uplift_multiplier_us": 1.1,
"search_context_cost_per_query": {
@ -76787,6 +77270,15 @@
"output_cost_per_token_batches": 2.5e-06,
"output_cost_per_token_flex": 2.5e-06,
"output_cost_per_token_priority": 1e-05,
"reasoning_contexts": [
"auto",
"current_turn",
"all_turns"
],
"reasoning_modes": [
"standard",
"pro"
],
"regional_processing_uplift_multiplier_eu": 1.1,
"regional_processing_uplift_multiplier_us": 1.1,
"search_context_cost_per_query": {"search_context_size_high":0.01,"search_context_size_low":0.01,"search_context_size_medium":0.01},

View file

@ -4437,6 +4437,32 @@
"generateContent": true
}
},
"zerogpu": {
"display_name": "ZeroGPU (`zerogpu`)",
"url": "https://docs.litellm.ai/docs/providers/zerogpu",
"endpoints": {
"chat_completions": true,
"responses": true,
"text_completion": true,
"embeddings": false,
"image_generations": false,
"image_edits": false,
"audio_transcriptions": false,
"audio_speech": false,
"rerank": false,
"ocr": false,
"search": false,
"moderations": false,
"realtime": false,
"video_generations": false,
"batches": false,
"messages": true,
"interactions": true,
"systemone": false,
"a2a": true,
"generateContent": true
}
},
"ragflow": {
"display_name": "RAGFlow (`ragflow`)",
"url": "https://docs.litellm.ai/docs/providers/ragflow",

View file

@ -37,6 +37,7 @@ from litellm.litellm_core_utils.prompt_templates.common_utils import (
schema_has_unresolvable_ref,
schema_property_count,
split_concatenated_json_objects,
split_media_for_tpm,
strip_encrypted_reasoning_from_messages,
strip_litellm_response_only_fields,
system_messages_first,
@ -2512,3 +2513,118 @@ def test_shared_image_url_agrees_with_responses_normalization(
expected: Final = {"type": "image_url", "image_url": {"url": "data:image/png;base64,IMAGE"}}
assert as_openai_image_part(part) == expected
assert as_openai_image_part(normalized) == expected
@pytest.mark.parametrize(
"part,projected,media",
[
(
{"type": "image_url", "image_url": {"url": "data:image/png;base64,IMAGE", "detail": "high"}},
{"type": "image_url", "image_url": {"url": "", "detail": "high"}},
{"type": "image_url", "image_url": {"url": "data:image/png;base64,IMAGE", "detail": "high"}},
),
(
{"type": "input_image", "image_url": "https://example.com/image.png", "detail": "low"},
{"type": "input_image", "image_url": "", "detail": "low"},
{"type": "image_url", "image_url": {"url": "https://example.com/image.png", "detail": "low"}},
),
(
{"type": "computer_screenshot", "image_url": "data:image/png;base64,IMAGE"},
{"type": "computer_screenshot", "image_url": ""},
{"type": "image_url", "image_url": {"url": "data:image/png;base64,IMAGE", "detail": "auto"}},
),
(
{"type": "image", "source": {"type": "base64", "media_type": "image/png", "data": "IMAGE"}},
{"type": "image", "source": {"type": "base64", "media_type": "image/png", "data": ""}},
{"type": "image_url", "image_url": {"url": "data:image/png;base64,IMAGE", "detail": "auto"}},
),
(
{"type": "input_audio", "input_audio": {"data": "AUDIO", "format": "wav"}},
{"type": "input_audio", "input_audio": {"data": "", "format": "wav"}},
{"type": "input_audio", "input_audio": {"data": "AUDIO", "format": "wav"}},
),
(
{"type": "file", "file": {"file_data": "IMAGE", "format": "image/png", "filename": "image.png"}},
{"type": "file", "file": {"file_data": "", "format": "image/png", "filename": "image.png"}},
{"type": "image_url", "image_url": {"url": "data:image/png;base64,IMAGE", "detail": "auto"}},
),
(
{"type": "input_file", "file_data": "data:audio/wav;base64,AUDIO", "filename": "audio.wav"},
{"type": "input_file", "file_data": "", "filename": "audio.wav"},
{"type": "input_audio", "input_audio": {"data": "AUDIO", "format": "wav"}},
),
],
)
def test_tpm_projection_blanks_only_declared_media_transport(
part: Mapping[str, object], projected: Mapping[str, object], media: Mapping[str, object],
) -> None:
text: Final = {"type": "text", "text": "data:image/png;base64,IMAGE"}
request: Final = [{"role": "user", "content": [text, {**part, "cache_control": {"type": "ephemeral"}}]}]
original: Final = copy.deepcopy(request)
assert split_media_for_tpm(request) == (
[{"role": "user", "content": [text, {**projected, "cache_control": {"type": "ephemeral"}}]}],
(media,),
)
assert request == original
def test_tpm_projection_retains_nested_and_native_history_around_media() -> None:
image: Final = {"type": "image_url", "image_url": {"url": "data:image/png;base64,IMAGE", "detail": "high"}}
empty_image: Final = {"type": "image_url", "image_url": {"url": "", "detail": "high"}}
document: Final = {"type": "document", "title": "keep", "source": {"type": "content", "content": [image]}}
empty_document: Final = {**document, "source": {"type": "content", "content": [empty_image]}}
tool: Final = {"type": "tool_result", "tool_use_id": "call", "content": [document, {"type": "text", "text": "keep"}]}
empty_tool: Final = {**tool, "content": [empty_document, {"type": "text", "text": "keep"}]}
request: Final = [
{"role": "user", "content": [tool]},
{"type": "function_call_output", "call_id": "call", "output": [image]},
{"type": "custom_tool_call_output", "call_id": "custom", "output": [image]},
{"type": "computer_call_output", "call_id": "computer", "output": {
"type": "computer_screenshot", "image_url": "data:image/png;base64,IMAGE",
}},
{"type": "image_generation_call", "id": "ig", "result": "IMAGE", "status": "completed"},
{"type": "code_interpreter_call", "id": "ci", "code": "keep()", "outputs": [
{"type": "logs", "logs": "keep"}, {"type": "image", "url": "https://example.com/image.png"},
]},
]
original: Final = copy.deepcopy(request)
projected, media = split_media_for_tpm(request)
assert projected == [
{"role": "user", "content": [empty_tool]},
{"type": "function_call_output", "call_id": "call", "output": [empty_image]},
{"type": "custom_tool_call_output", "call_id": "custom", "output": [empty_image]},
{"type": "computer_call_output", "call_id": "computer", "output": {
"type": "computer_screenshot", "image_url": "",
}},
{"type": "image_generation_call", "id": "ig", "result": "", "status": "completed"},
{"type": "code_interpreter_call", "id": "ci", "code": "keep()", "outputs": [
{"type": "logs", "logs": "keep"}, {"type": "image", "url": ""},
]},
]
assert media == (
image, image, image,
{"type": "image_url", "image_url": {"url": "data:image/png;base64,IMAGE", "detail": "auto"}},
{"type": "image_url", "image_url": {"url": "data:application/octet-stream;base64,IMAGE", "detail": "auto"}},
{"type": "image_url", "image_url": {"url": "https://example.com/image.png", "detail": "auto"}},
)
assert request == original
def test_tpm_projection_preserves_non_media_identity_and_opaque_values() -> None:
image: Final = {"type": "input_image", "image_url": "data:image/png;base64,OPAQUE"}
request: Final = [
{"role": "user", "content": json.dumps(image)},
{"type": "custom_tool_call_output", "call_id": "call", "output": json.dumps(image)},
{"type": "function_call", "call_id": "call", "name": "keep", "arguments": {"content": [image]}},
{"type": "file_search_call", "results": [{"text": "keep", "content": [image]}]},
{"type": "image_generation_call", "id": "ig", "status": "in_progress", "result": None},
{"role": "user", "content": [
{"type": "document", "source": {"type": "base64", "media_type": "application/pdf", "data": "PDF"}},
{"type": "document", "source": {"type": "text", "data": json.dumps(image)}},
{"type": "file", "file": {"format": "application/pdf", "file_data": "PDF"}},
{"type": "text", "text": "keep", "metadata": {"content": [image]}},
]},
]
projected, media = split_media_for_tpm(request)
assert projected is request
assert media == ()

View file

@ -2,16 +2,23 @@
Test the audio utils functionality in litellm_core_utils/audio_utils/utils.py
"""
import base64
import io
import os
import tempfile
from typing import Final
from unittest.mock import mock_open, patch
import pytest
import soundfile as sf
from litellm.litellm_core_utils.audio_utils.utils import (
AUDIO_BYTES_PER_TOKEN,
AUDIO_TOKENS_PER_SECOND,
DEFAULT_AUDIO_TOKEN_ESTIMATE,
ProcessedAudioFile,
calculate_request_duration,
estimate_audio_tokens,
get_audio_file_content_hash,
get_audio_file_for_health_check,
get_audio_file_name,
@ -19,6 +26,63 @@ from litellm.litellm_core_utils.audio_utils.utils import (
)
def _encoded_audio(seconds: int, sample_rate: int, channels: int, container: str) -> str:
buffer: Final = io.BytesIO()
with sf.SoundFile(buffer, "w", samplerate=sample_rate, channels=channels, format=container) as audio:
audio.buffer_write(b"\0" * (seconds * sample_rate * channels * 2), dtype="int16")
return base64.b64encode(buffer.getvalue()).decode()
@pytest.mark.parametrize(
("sample_rate", "channels", "container"), [(8000, 1, "WAV"), (48000, 2, "WAV"), (24000, 1, "FLAC")]
)
def test_audio_estimate_uses_duration_independent_of_sample_rate_channels_and_compression(
sample_rate: int, channels: int, container: str
) -> None:
assert estimate_audio_tokens(_encoded_audio(1, sample_rate, channels, container)) == AUDIO_TOKENS_PER_SECOND
assert estimate_audio_tokens(_encoded_audio(31, sample_rate, channels, container)) == 31 * AUDIO_TOKENS_PER_SECOND
# Audio rates verified 2026-10-28: https://ai.google.dev/gemini-api/docs/generate-content/media-resolution and
# https://developers.openai.com/api/docs/guides/voice-latency-cost
@pytest.mark.parametrize(
("models", "fallback", "expected"),
[
(("openai/gpt-audio-1.5",), "gemini-3.8-flash", 10),
(("gpt-audio-1.5", "gemini/gemini-3.8-flash"), None, 25),
(("vertex_ai/gemini-3.8-flash",), None, 25),
(("openrouter/google/gemini-3.8-flash",), None, 25),
(("azure/audio-deployment",), "gpt-audio-1.5", 10),
(("opaque-deployment",), "gemini-3.8-flash", 25),
((), "gemini-3.8-flash", 25),
(("gemini/gemini-2.5-flash", "gemini/gemini-3-pro-preview"), None, 32),
(("other/gemini-3.8-flash",), None, 10),
],
)
def test_audio_estimate_prefers_each_backend_rate_over_the_requested_model(
models: tuple[str, ...], fallback: str | None, expected: int
) -> None:
assert estimate_audio_tokens(_encoded_audio(1, 8000, 1, "WAV"), models, fallback) == expected
@pytest.mark.parametrize("data", [None, "", "invalid-base64!", base64.b64encode(b"unreadable audio").decode()])
def test_audio_estimate_retains_nonzero_fallback_for_unreadable_audio(data: str | None) -> None:
assert estimate_audio_tokens(data) == DEFAULT_AUDIO_TOKEN_ESTIMATE
def test_audio_estimate_retains_size_based_fallback_for_large_unreadable_audio() -> None:
data: Final = base64.b64encode(b"\0" * (AUDIO_BYTES_PER_TOKEN * 400)).decode()
assert estimate_audio_tokens(data) == 400
def test_audio_estimate_bounds_base64_allocation_before_decoding() -> None:
data: Final = base64.b64encode(b"\0" * (16 * 1024 * 1024 + 1)).decode()
with patch("base64.b64decode", side_effect=AssertionError("oversized audio must not be decoded")) as decode:
estimate: Final = estimate_audio_tokens(data)
assert estimate == len(data) * 3 // 4 // AUDIO_BYTES_PER_TOKEN
decode.assert_not_called()
class TestProcessAudioFile:
"""Test the process_audio_file function with various input types"""

View file

@ -37,6 +37,7 @@ from litellm.litellm_core_utils.token_counter import (
_get_extrapolating_count_function,
_get_tiktoken_count_function,
calculate_img_tokens,
estimate_image_tokens,
high_detail_image_token_upper_bound,
offload_token_count,
)
@ -2152,6 +2153,136 @@ def test_high_detail_image_token_upper_bound_is_reached_by_the_largest_high_res_
assert calculate_img_tokens(_png_data_url(1, 1), mode="high") < high_detail_image_token_upper_bound()
# Image sizing verified 2026-10-28: https://developers.openai.com/api/docs/guides/images-vision
# and https://platform.claude.com/docs/en/build-with-claude/vision-coordinates
@pytest.mark.parametrize(
("model", "width", "height", "detail", "expected"),
[
("gpt-4o-mini", 1024, 1024, "low", 2833),
("gpt-4o-mini", 1024, 1024, "high", 25501),
("gpt-5.1", 512, 512, "auto", 210),
("gpt-6-astra", 2048, 2048, "high", 3000),
("gpt-6-astra", 4096, 512, "high", 2458),
("gpt-6-astra", 2048, 2048, "auto", 4916),
("gpt-5.6-sol", 2048, 2048, "low", 308),
("gpt-5.5", 6400, 6400, "original", 12000),
("gpt-5.4-mini", 2048, 2048, "low", 4916),
("gpt-5.4-nano", 2048, 2048, "auto", 3000),
("gpt-5.2", 4096, 512, "auto", 615),
("gpt-4.1-mini-2025-04-14", 1024, 1024, "low", 1659),
("claude-haiku-5-5", 200, 200, "auto", 64),
("anthropic/claude-haiku-5-5", 1024, 1024, "auto", 1369),
("claude-haiku-5-5", 1920, 1080, "low", 2691),
("claude-haiku-4-5", 1920, 1080, "auto", 1560),
("claude-haiku-4-5", 1075, 1520, "auto", 1551),
("claude-haiku-5-5", 3840, 2160, "auto", 4784),
("claude-haiku-5-5", 8000, 100, "auto", 184),
],
)
def test_image_estimate_uses_model_resolution_and_detail(
model: str, width: int, height: int, detail: str, expected: int
) -> None:
assert estimate_image_tokens(model, _png_data_url(width, height), detail) == expected
@pytest.mark.parametrize(
("models", "fallback", "expected_model"),
[
(("azure/deployment-prod",), "gpt-4o", "gpt-4o"),
(("openai/gpt-4o",), "gpt-4o-mini", "gpt-4o"),
(("azure/deployment-prod", "openai/gpt-4o"), "gpt-4o-mini", "gpt-4o-mini"),
(("openai/gpt-4o", "openrouter/openai/gpt-4o-mini"), None, "gpt-4o-mini"),
(("azure_ai/openai/gpt-6-astra",), None, "gpt-6-astra"),
(("azure/gpt-41",), None, "gpt-4.1"),
(("azure/eu/gpt-4o-mini",), None, "gpt-4o-mini"),
(("unknown", "gpt-4o-mini"), None, "gpt-4o-mini"),
((), "gpt-6-astra", "gpt-6-astra"),
(("other/gpt-4o-mini",), None, "unregistered-model"),
(("azure/opaque/gpt-4o-mini",), None, "unregistered-model"),
(("bedrock/anthropic.claude-haiku-5-5",), None, "claude-haiku-5-5"),
(("bedrock/us.anthropic.claude-sonnet-4-20250514-v1:0",), None, "claude-haiku-4-5"),
(("vertex_ai/claude-haiku-5-5@default",), None, "claude-haiku-5-5"),
(("azure_ai/claude-haiku-5-5",), None, "claude-haiku-5-5"),
(("anthropic/claude-haiku-5-5", "openai/gpt-4o"), "gpt-4o-mini", "claude-haiku-5-5"),
(("other/claude-haiku-5-5",), None, "unregistered-model"),
],
)
def test_image_estimate_prefers_each_backend_policy_over_the_requested_model(
models: tuple[str, ...], fallback: str | None, expected_model: str
) -> None:
data: Final = _png_data_url(1024, 1024)
assert estimate_image_tokens(models, data, "high", fallback_model=fallback) == estimate_image_tokens(
expected_model, data, "high"
)
@pytest.mark.parametrize(
("model", "detail"),
[
("unknown", "high"),
("claude-unregistered", "auto"),
("gpt-4o", "original"),
("gpt-5.2", "original"),
("gpt-6-astra", "invalid"),
],
)
def test_image_estimate_preserves_default_when_policy_is_unavailable(model: str, detail: str) -> None:
assert (
estimate_image_tokens(model, _png_data_url(1024, 1024), detail) == litellm.constants.DEFAULT_IMAGE_TOKEN_COUNT
)
@pytest.mark.parametrize(
"data",
[
"https://image.invalid/photo.png",
"data:image/png;base64,invalid!",
"data:application/octet-stream;base64,bm90LWFuLWltYWdl",
"data:image/jpeg;base64,/9j/4AAQ",
"data:image/webp;base64,UklGRgAAAABXRUJQ",
_png_data_url(0, 1024),
],
)
# Accepted patch limits verified 2026-10-28: https://developers.openai.com/api/docs/guides/images-vision
# and https://platform.claude.com/docs/en/build-with-claude/vision
@pytest.mark.parametrize(
("model", "detail", "bound"),
[
("gpt-4o", "high", high_detail_image_token_upper_bound()),
("gpt-6-astra", "high", 3000),
("gpt-6-astra", "auto", 36000),
("claude-haiku-5-5", "auto", 4784),
("claude-haiku-4-5", "auto", 1568),
],
)
def test_image_estimate_uses_bounded_fallback_without_fetching_media(
data: str, model: str, detail: str, bound: int
) -> None:
with patch("httpx.Client.send") as send:
estimate: Final = estimate_image_tokens(model, data, detail)
send.assert_not_called()
assert estimate == bound
assert estimate > estimate_image_tokens(model, _png_data_url(1024, 1024), detail)
@pytest.mark.parametrize("container", ["PNG", "JPEG", "GIF", "WEBP"])
def test_image_estimate_depends_on_dimensions_and_not_compressed_payload_size(container: str) -> None:
output: Final = io.BytesIO()
Image.new("RGB", (1024, 1024)).save(output, format=container)
data: Final = "data:application/octet-stream;base64," + base64.b64encode(output.getvalue()).decode()
assert estimate_image_tokens("gpt-6-astra", data) == estimate_image_tokens("gpt-6-astra", _png_data_url(1024, 1024))
def test_image_estimate_bounds_header_decoding_for_large_inline_payloads() -> None:
header: Final = base64.b64decode(_png_data_url(1024, 1024).split(",", 1)[1])
data: Final = "data:image/png;base64," + base64.b64encode(header + b"\0" * 4_000_000).decode()
with patch("base64.b64decode", wraps=base64.b64decode) as decode_header:
estimate: Final = estimate_image_tokens("gpt-6-astra", data)
assert estimate == estimate_image_tokens("gpt-6-astra", _png_data_url(1024, 1024))
assert decode_header.call_count == 1
assert len(decode_header.call_args.args[0]) <= 65_536
HUB_SETUP_SCRIPT: Final = """
import json
import sys

View file

@ -0,0 +1,239 @@
import json
from collections.abc import Mapping
from typing import Final
import httpx
import pytest
import respx
from openai import OpenAI
import litellm
from litellm.litellm_core_utils.get_llm_provider_logic import get_llm_provider
from litellm.proxy.auth.model_checks import get_provider_models
ZEROGPU_API_BASE: Final = "https://api.zerogpu.ai/v1"
ZEROGPU_MODELS: Final = (
"deepseek-v4.1-flash",
"glm-5.3-flash",
"gpt-4.1-mini",
"gpt-5.4-nano",
"gpt-5.6-luna",
"gpt-6-luna",
"gpt-oss-120b",
"LFM2.5-1.2B-Instruct",
"LFM2.5-1.2B-Thinking",
"llama-3.1-8b-instruct-fast",
"llama-3.2-3b-instruct-fast",
"qwen3-30b-a3b-fp8",
"qwen3.6-35b-a3b",
)
WEATHER_TOOL: Final = {
"type": "function",
"function": {
"name": "get_weather",
"parameters": {"type": "object", "properties": {"city": {"type": "string"}}},
},
}
class _RecordingZeroGPU:
def __init__(self, usage: Mapping[str, object]) -> None:
self.usage: Final = usage
self.requests: tuple[httpx.Request, ...] = ()
def respond(self, request: httpx.Request) -> httpx.Response:
self.requests = (*self.requests, request)
return httpx.Response(
200,
json={
"id": "chatcmpl-zerogpu",
"object": "chat.completion",
"created": 1234567890,
"model": json.loads(request.content)["model"],
"choices": [{"index": 0, "message": {"role": "assistant", "content": "hi"}, "finish_reason": "stop"}],
"usage": dict(self.usage),
},
)
def client(self) -> OpenAI:
return OpenAI(
api_key="zerogpu-test-key",
base_url=ZEROGPU_API_BASE,
http_client=httpx.Client(transport=httpx.MockTransport(self.respond)),
)
def test_prefixed_model_reaches_zerogpu_chat_completions_without_the_prefix():
zerogpu: Final = _RecordingZeroGPU({"prompt_tokens": 3, "completion_tokens": 1, "total_tokens": 4})
with zerogpu.client() as client:
litellm.completion(
model="zerogpu/gpt-oss-120b",
messages=[{"role": "user", "content": "hello"}],
client=client,
)
assert len(zerogpu.requests) == 1
assert str(zerogpu.requests[0].url) == f"{ZEROGPU_API_BASE}/chat/completions"
assert json.loads(zerogpu.requests[0].content) == {
"model": "gpt-oss-120b",
"messages": [{"role": "user", "content": "hello"}],
}
def test_responses_request_reaches_zerogpu_responses_natively():
with respx.mock() as upstream:
route: Final = upstream.post(f"{ZEROGPU_API_BASE}/responses").respond(
200,
json={
"id": "resp_zerogpu",
"object": "response",
"created_at": 1_792_000_000,
"model": "gpt-oss-120b",
"status": "completed",
"output": [
{
"id": "msg_zerogpu",
"type": "message",
"role": "assistant",
"status": "completed",
"content": [{"type": "output_text", "text": "hi", "annotations": []}],
}
],
"usage": {"input_tokens": 3, "output_tokens": 1, "total_tokens": 4},
},
)
response: Final = litellm.responses(model="zerogpu/gpt-oss-120b", input="hello", api_key="zerogpu-test-key")
body: Final = json.loads(route.calls.last.request.content)
assert body["model"] == "gpt-oss-120b"
assert body["input"] == "hello"
assert response.output[0].content[0].text == "hi"
def test_anthropic_messages_request_is_bridged_to_zerogpu_chat_completions():
zerogpu: Final = _RecordingZeroGPU({"prompt_tokens": 3, "completion_tokens": 1, "total_tokens": 4})
with zerogpu.client() as client:
response: Final = litellm.anthropic.messages.create(
model="zerogpu/gpt-oss-120b",
messages=[{"role": "user", "content": "hello"}],
max_tokens=10,
client=client,
)
assert [str(request.url) for request in zerogpu.requests] == [f"{ZEROGPU_API_BASE}/chat/completions"]
assert json.loads(zerogpu.requests[0].content) == {
"model": "gpt-oss-120b",
"messages": [{"role": "user", "content": "hello"}],
"max_tokens": 10,
}
assert response["content"] == [{"type": "text", "text": "hi"}]
@pytest.mark.parametrize("model", ZEROGPU_MODELS)
def test_zerogpu_completion_is_charged_at_the_cost_map_rates(model: str):
usage: Final = {
"prompt_tokens": 1000,
"completion_tokens": 200,
"total_tokens": 1200,
"prompt_tokens_details": {"cached_tokens": 400},
}
zerogpu: Final = _RecordingZeroGPU(usage)
with zerogpu.client() as client:
response: Final = litellm.completion(
model=f"zerogpu/{model}",
messages=[{"role": "user", "content": "hello"}],
client=client,
)
rates: Final = litellm.model_cost[f"zerogpu/{model}"]
cached_rate: Final = rates.get("cache_read_input_token_cost", rates["input_cost_per_token"])
expected_cost: Final = (
600 * rates["input_cost_per_token"] + 400 * cached_rate + 200 * rates["output_cost_per_token"]
)
assert response._hidden_params["response_cost"] == pytest.approx(expected_cost)
assert expected_cost > 0
@pytest.mark.parametrize("model", ZEROGPU_MODELS)
def test_trim_messages_fits_zerogpu_history_into_the_model_context_window(model: str):
budget: Final = 20
trim_ratio: Final = budget / litellm.model_cost[f"zerogpu/{model}"]["max_input_tokens"]
messages: Final = [{"role": "user", "content": "lorem ipsum " * 300}]
trimmed: Final = litellm.utils.trim_messages(messages, model=f"zerogpu/{model}", trim_ratio=trim_ratio)
trimmed_tokens: Final = litellm.token_counter(model=f"zerogpu/{model}", messages=trimmed)
assert trimmed_tokens <= budget, trimmed
@pytest.mark.parametrize(
("model", "forwards_tools", "forwards_reasoning_effort"),
[
("gpt-5.6-luna", True, True),
("gpt-oss-120b", True, True),
("qwen3.6-35b-a3b", True, True),
("LFM2.5-1.2B-Instruct", False, False),
],
)
def test_tools_and_reasoning_effort_reach_zerogpu_only_for_models_that_support_them(
model: str, forwards_tools: bool, forwards_reasoning_effort: bool
):
zerogpu: Final = _RecordingZeroGPU({"prompt_tokens": 3, "completion_tokens": 1, "total_tokens": 4})
with zerogpu.client() as client:
litellm.completion(
model=f"zerogpu/{model}",
messages=[{"role": "user", "content": "weather in Paris?"}],
tools=[WEATHER_TOOL],
tool_choice="auto",
reasoning_effort="high",
drop_params=True,
client=client,
)
tools: Final = {"tools": [WEATHER_TOOL], "tool_choice": "auto"} if forwards_tools else {}
reasoning: Final = {"reasoning_effort": "high"} if forwards_reasoning_effort else {}
assert json.loads(zerogpu.requests[0].content) == {
"model": model,
"messages": [{"role": "user", "content": "weather in Paris?"}],
**tools,
**reasoning,
}
@pytest.mark.parametrize("model", ZEROGPU_MODELS)
def test_model_info_reports_the_reasoning_support_litellm_routes_with(model: str):
model_info: Final = litellm.get_model_info(f"zerogpu/{model}")
assert bool(model_info.get("supports_reasoning")) == litellm.utils.supports_reasoning(f"zerogpu/{model}")
def test_zerogpu_key_and_base_come_from_zerogpu_env_vars(monkeypatch: pytest.MonkeyPatch):
monkeypatch.setenv("ZEROGPU_API_KEY", "zerogpu-env-key")
monkeypatch.delenv("ZEROGPU_API_BASE", raising=False)
assert get_llm_provider(model="zerogpu/gpt-oss-120b") == (
"gpt-oss-120b",
"zerogpu",
"zerogpu-env-key",
ZEROGPU_API_BASE,
)
monkeypatch.setenv("ZEROGPU_API_BASE", "https://zerogpu.internal.example/v1")
assert get_llm_provider(model="zerogpu/gpt-oss-120b")[3] == "https://zerogpu.internal.example/v1"
def test_zerogpu_wildcard_lists_every_zerogpu_catalog_model():
catalog_models: Final = sorted(
key for key, row in litellm.model_cost.items() if row.get("litellm_provider") == "zerogpu"
)
assert catalog_models
assert sorted(get_provider_models("zerogpu") or []) == catalog_models

View file

@ -15,28 +15,38 @@ Redis.
"""
import asyncio
import base64
import copy
import io
import json
from datetime import datetime, timedelta
from typing import Any, Dict, Final
import pytest
import soundfile as sf
from fastapi import HTTPException
from PIL import Image
from litellm import token_counter
from litellm.caching.caching import DualCache
from litellm.litellm_core_utils.token_counter import UnsupportedContentTypeError, estimate_image_tokens
from litellm.proxy._types import UserAPIKeyAuth
from litellm.proxy.hooks.parallel_request_limiter_v3 import (
_AUDIO_BYTES_PER_TOKEN,
DEFAULT_AUDIO_TOKEN_ESTIMATE,
PROJECT_ITPM_DESCRIPTOR_KEY,
PROJECT_OTPM_DESCRIPTOR_KEY,
RateLimitedModel,
_AUDIO_BYTES_PER_TOKEN,
PROXY_MaxParallelRequestsHandler_v3 as RateLimitHandler,
)
from litellm.proxy.hooks.parallel_request_limiter_v3 import (
_call_id_from_callback_kwargs,
_request_stash,
get_or_create_request_stash,
get_request_stash,
)
from litellm.proxy.hooks.parallel_request_limiter_v3 import (
PROXY_MaxParallelRequestsHandler_v3 as RateLimitHandler,
)
from litellm.proxy.utils import InternalUsageCache, hash_token
from litellm.router_utils.ptu_shares import PTUTeamCeiling
from litellm.types.llms.openai import (
InputTokensDetails,
ResponseAPIUsage,
@ -3775,3 +3785,284 @@ def test_reservation_counts_a_compaction_item_by_its_summary(rate_limiter):
) == handler._estimate_input_and_output_tokens(
data={"model": "gpt-4o", "input": _COMPACTED_INPUT[2:], "max_output_tokens": 1}, call_type="aresponses"
)
_QUOTA_TEXT: Final = "a" * 400
_QUOTA_JSON_TEXT: Final = '{"type":"input_image","image_url":"data:image/png;base64,AAAA"}'
@pytest.mark.parametrize(
("call_type", "request_data", "expected"),
[
("acompletion", {"messages": [{"role": "user", "content": _QUOTA_TEXT}], "max_tokens": 16}, (100, 16)),
("acompletion", {"messages": [{"role": "user", "content": _QUOTA_JSON_TEXT}], "max_tokens": 16}, (15, 16)),
(
"anthropic_messages",
{
"messages": [
{
"role": "user",
"content": [
{
"type": "tool_result",
"tool_use_id": "t",
"content": [{"type": "text", "text": _QUOTA_TEXT}],
}
],
}
],
"max_tokens": 16,
},
(0, 16),
),
(
"acompletion",
{
"messages": [
{
"role": "assistant",
"content": None,
"tool_calls": [
{"id": "t", "type": "function", "function": {"name": "f", "arguments": _QUOTA_TEXT}}
],
}
],
"max_tokens": 16,
},
(0, 16),
),
("aresponses", {"input": _QUOTA_TEXT, "max_output_tokens": 16}, (100, 16)),
("aresponses", {"input": _QUOTA_TEXT, "instructions": "b" * 400, "max_output_tokens": 16}, (100, 16)),
(
"aresponses",
{"input": [{"role": "user", "content": _QUOTA_TEXT}], "instructions": "b" * 400, "max_output_tokens": 16},
(107, 16),
),
(
"aresponses",
{
"input": [
{
"type": "file_search_call",
"id": "f",
"status": "completed",
"queries": ["q"],
"results": [
{
"file_id": "file_1",
"filename": "result.txt",
"attributes": {},
"score": 1,
"text": _QUOTA_TEXT,
}
],
}
],
"max_output_tokens": 16,
},
(146, 16),
),
(
"aresponses",
{
"input": [{"type": "custom_tool_call_output", "call_id": "t", "output": _QUOTA_JSON_TEXT}],
"max_output_tokens": 16,
},
(32, 16),
),
("aresponses", {"input": "", "instructions": "b" * 400}, (0, 0)),
("aembedding", {"input": [_QUOTA_TEXT]}, (100, 0)),
("atext_completion", {"prompt": [_QUOTA_TEXT], "max_tokens": 16}, (100, 16)),
],
)
def test_media_correction_preserves_exact_non_media_reservations(
rate_limiter: tuple[RateLimitHandler, DualCache],
call_type: str,
request_data: dict[str, object],
expected: tuple[int, int],
) -> None:
handler, _ = rate_limiter
assert handler._estimate_input_and_output_tokens(request_data, call_type=call_type) == expected
def _quota_media_payload(kind: str, compressed: bool) -> str:
output: Final = io.BytesIO()
if kind == "image":
Image.new("RGB", (1024, 1024)).save(output, format="PNG", compress_level=9 if compressed else 0)
else:
sample_rate: Final = 8000 if compressed else 48000
with sf.SoundFile(output, "w", samplerate=sample_rate, channels=1, format="WAV") as audio:
audio.buffer_write(b"\0" * (sample_rate * 2), dtype="int16")
return base64.b64encode(output.getvalue()).decode()
def _quota_media_request(kind: str, payload: str, call_type: str) -> dict[str, object]:
block: Final = (
{"type": "input_audio", "input_audio": {"data": payload, "format": "wav"}}
if kind == "audio"
else {"type": "input_image", "image_url": "data:image/png;base64," + payload}
if call_type == "aresponses"
else {"type": "image", "source": {"type": "base64", "media_type": "image/png", "data": payload}}
if call_type == "anthropic_messages"
else {"type": "image_url", "image_url": {"url": "data:image/png;base64," + payload}}
)
content: Final = (
[{"type": "tool_result", "tool_use_id": "t", "content": [block]}]
if call_type == "anthropic_messages"
else [block]
)
field: Final = "input" if call_type == "aresponses" else "messages"
return {
"model": "gpt-6-astra",
field: [{"role": "user", "content": content}],
"max_tokens": 16,
"max_output_tokens": 16,
}
@pytest.mark.parametrize(
("kind", "call_type"),
[
("image", "acompletion"),
("image", "aresponses"),
("image", "anthropic_messages"),
("audio", "acompletion"),
("audio", "aresponses"),
],
)
def test_media_reservation_is_independent_of_encoding_and_preserves_request(
rate_limiter: tuple[RateLimitHandler, DualCache], kind: str, call_type: str
) -> None:
handler, _ = rate_limiter
compressed: Final = _quota_media_request(kind, _quota_media_payload(kind, True), call_type)
uncompressed: Final = _quota_media_request(kind, _quota_media_payload(kind, False), call_type)
original: Final = copy.deepcopy(uncompressed)
estimate: Final = handler._estimate_input_and_output_tokens(uncompressed, call_type=call_type)
assert estimate == handler._estimate_input_and_output_tokens(compressed, call_type=call_type)
assert estimate[0] > 0 and estimate[1] == 16
assert uncompressed == original
@pytest.mark.asyncio
@pytest.mark.parametrize("call_type", ["acompletion", "aresponses"])
@pytest.mark.parametrize("limit", [1000, 20000])
async def test_media_pre_call_rejects_tight_quota_and_reserves_sufficient_quota(
rate_limiter: tuple[RateLimitHandler, DualCache], call_type: str, limit: int
) -> None:
handler, cache = rate_limiter
request: Final = {
key: value
for key, value in _quota_media_request("image", _quota_media_payload("image", True), call_type).items()
if key not in ("max_tokens", "max_output_tokens")
}
auth: Final = UserAPIKeyAuth(api_key=hash_token("sk-media-quota"), tpm_limit=limit)
if limit == 1000:
with pytest.raises(HTTPException) as error:
await handler.async_pre_call_hook(user_api_key_dict=auth, cache=cache, data=request, call_type=call_type)
assert error.value.status_code == 429
return
await handler.async_pre_call_hook(user_api_key_dict=auth, cache=cache, data=request, call_type=call_type)
stash: Final = get_request_stash()
assert stash is not None
expected: Final = handler._estimate_input_and_output_tokens(
request, call_type=call_type, min_configured_tpm_limit=limit
)
assert stash.reserved_tokens == sum(expected)
assert expected[0] > 1000 and expected[1] >= expected[0]
@pytest.mark.asyncio
async def test_ptu_pre_call_weights_media_input_and_output() -> None:
cache: Final = DualCache()
ceiling: Final = PTUTeamCeiling(
model_group="gpt-6-astra", tpm_limit=10000, output_to_input_ratio=4, cached_input_ratio=0
)
def resolve_ceiling(team: str, model: str) -> PTUTeamCeiling | None:
return ceiling if (team, model) == ("test-team", "gpt-6-astra") else None
handler: Final = RateLimitHandler(
internal_usage_cache=InternalUsageCache(cache), ptu_team_ceiling_resolver=resolve_ceiling
)
request: Final = _quota_media_request("image", _quota_media_payload("image", True), "acompletion")
await handler.async_pre_call_hook(
user_api_key_dict=UserAPIKeyAuth(api_key=hash_token("sk-media-ptu"), team_id="test-team"),
cache=cache,
data=request,
call_type="acompletion",
)
stash: Final = get_request_stash()
assert stash is not None
assert (
stash.ptu_reserved_tokens
== estimate_image_tokens("gpt-6-astra", "data:image/png;base64," + _quota_media_payload("image", True)) + 4 * 16
)
@pytest.mark.asyncio
async def test_project_fallback_keeps_legacy_audio_estimate_and_output_policy(
rate_limiter: tuple[RateLimitHandler, DualCache],
) -> None:
handler, cache = rate_limiter
unsupported: Final = {"type": "unsupported_content"}
with pytest.raises(UnsupportedContentTypeError):
token_counter(model="gpt-6-astra", messages=[{"role": "user", "content": [unsupported]}])
request: Final = {
"model": "gpt-6-astra",
"messages": [
{
"role": "user",
"content": [
unsupported,
{
"type": "input_audio",
"input_audio": {"data": _quota_media_payload("audio", True), "format": "wav"},
},
],
}
],
}
auth: Final = UserAPIKeyAuth(
api_key=hash_token("sk-media-project"),
project_id="media-project",
project_metadata={"model_itpm_limit": {"gpt-6-astra": 1000}, "model_otpm_limit": {"gpt-6-astra": 1000}},
)
await handler.async_pre_call_hook(user_api_key_dict=auth, cache=cache, data=request, call_type="acompletion")
stash: Final = get_request_stash()
assert stash is not None
assert stash.itpm_reserved_tokens == DEFAULT_AUDIO_TOKEN_ESTIMATE
assert stash.otpm_reserved_tokens == 1
@pytest.mark.asyncio
async def test_single_anthropic_image_exceeds_tight_quota_with_explicit_output_cap(
rate_limiter: tuple[RateLimitHandler, DualCache],
) -> None:
handler, cache = rate_limiter
request: Final = {
"model": "anthropic/claude-haiku-5-5",
"messages": [
{
"role": "user",
"content": [
{
"type": "image",
"source": {
"type": "base64",
"media_type": "image/png",
"data": _quota_media_payload("image", True),
},
}
],
}
],
"max_tokens": 64,
}
with pytest.raises(HTTPException) as error:
await handler.async_pre_call_hook(
user_api_key_dict=UserAPIKeyAuth(api_key=hash_token("sk-claude-media-quota"), tpm_limit=500),
cache=cache,
data=request,
call_type="anthropic_messages",
)
assert error.value.status_code == 429

View file

@ -363,6 +363,26 @@ def test_cognition_provider_fields():
assert fields_by_key["api_base"]["required"] is False
def test_zerogpu_add_model_form_defaults_resolve_to_a_priced_zerogpu_model():
import litellm
from litellm.litellm_core_utils.get_llm_provider_logic import get_llm_provider
app_instance: Final = FastAPI()
app_instance.include_router(router)
providers: Final = TestClient(app_instance).get("/public/providers/fields").json()
zerogpu: Final = next(p for p in providers if p["litellm_provider"] == "zerogpu")
fields_by_key: Final = {f["key"]: f for f in zerogpu["credential_fields"]}
placeholder: Final = zerogpu["default_model_placeholder"]
_, provider, _, api_base = get_llm_provider(model=placeholder)
assert provider == "zerogpu"
assert api_base == fields_by_key["api_base"]["placeholder"]
assert litellm.model_cost[placeholder]["litellm_provider"] == "zerogpu"
assert fields_by_key["api_key"]["required"] is True
assert fields_by_key["api_key"]["field_type"] == "password"
def test_qwen_mainland_provider_fields_carry_the_qianwen_brand():
app_instance = FastAPI()
app_instance.include_router(router)

View file

@ -2127,6 +2127,330 @@ def test_responses_bridge_preserves_reasoning_effort_with_drop_params(
assert request_body["reasoning"] == {"effort": "high"}
@pytest.fixture
def openai_responses_route(
restore_model_registry, respx_mock: respx.MockRouter, monkeypatch: pytest.MonkeyPatch
) -> respx.Route:
monkeypatch.setattr(litellm, "disable_aiohttp_transport", True)
monkeypatch.delenv("LITELLM_REASONING_AUTO_SUMMARY", raising=False)
litellm.register_model(
{
"gpt-6.1-terra": {
"litellm_provider": "openai",
"mode": "chat",
"supports_reasoning": True,
"supports_minimal_reasoning_effort": False,
"reasoning_modes": ["standard", "pro"],
"reasoning_contexts": ["auto", "current_turn", "all_turns"],
"input_cost_per_token": 0.0,
"output_cost_per_token": 0.0,
}
},
persist_across_reloads=False,
)
return respx_mock.post("https://api.openai.com/v1/responses").respond(
json={
"id": "resp_test",
"object": "response",
"created_at": 1734366691,
"status": "completed",
"model": "gpt-6.1-terra",
"output": [
{
"type": "message",
"id": "msg_1",
"status": "completed",
"role": "assistant",
"content": [{"type": "output_text", "text": "Done.", "annotations": []}],
}
],
"usage": {"input_tokens": 1, "output_tokens": 1, "total_tokens": 2},
}
)
@pytest.mark.parametrize(
("reasoning_effort", "summary_alias", "expected_reasoning"),
[
(
{"effort": "low", "mode": "pro", "context": "current_turn"},
{},
{"effort": "low", "mode": "pro", "context": "current_turn"},
),
(
{"effort": "low", "mode": "pro", "context": "current_turn"},
{"reasoning_summary": "auto"},
{"effort": "low", "mode": "pro", "context": "current_turn", "summary": "auto"},
),
(
{"effort": "low", "mode": "pro", "summary": "concise"},
{"reasoningSummary": "auto"},
{"effort": "low", "mode": "pro", "summary": "concise"},
),
(
{"effort": "low", "context": "current_turn", "summary": None},
{"reasoning_summary": "auto"},
{"effort": "low", "context": "current_turn", "summary": None},
),
({"mode": "pro", "context": "all_turns"}, {}, {"mode": "pro", "context": "all_turns"}),
("low", {}, {"effort": "low"}),
],
)
def test_openai_responses_bridge_sends_the_whole_reasoning_effort_dict(
reasoning_effort: str | dict[str, str | None],
summary_alias: dict[str, str],
expected_reasoning: dict[str, str | None],
openai_responses_route: respx.Route,
):
caller_reasoning_effort: Final = copy.deepcopy(reasoning_effort)
litellm.completion(
model="openai/responses/gpt-6.1-terra",
messages=[{"role": "user", "content": "hello"}],
reasoning_effort=reasoning_effort,
api_key="fake-key",
**summary_alias,
)
request_body: Final = json.loads(openai_responses_route.calls[0].request.content)
assert request_body["reasoning"] == expected_reasoning
assert reasoning_effort == caller_reasoning_effort
@pytest.mark.parametrize(
"reasoning_effort, summary_alias, expected_reasoning",
[
(
{"effort": "minimal", "mode": "pro", "context": "current_turn"},
{},
{"mode": "pro", "context": "current_turn"},
),
({"effort": "minimal", "mode": "pro", "summary": "auto"}, {}, {"mode": "pro", "summary": "auto"}),
({"effort": "minimal", "mode": "pro"}, {"reasoning_summary": "auto"}, {"mode": "pro", "summary": "auto"}),
],
)
def test_openai_responses_bridge_drops_only_a_refused_effort_under_drop_params(
reasoning_effort: dict[str, str],
summary_alias: dict[str, str],
expected_reasoning: dict[str, str],
openai_responses_route: respx.Route,
):
litellm.completion(
model="openai/responses/gpt-6.1-terra",
messages=[{"role": "user", "content": "hello"}],
reasoning_effort=reasoning_effort,
drop_params=True,
api_key="fake-key",
**summary_alias,
)
request_body: Final = json.loads(openai_responses_route.calls[0].request.content)
assert request_body["reasoning"] == expected_reasoning
def test_openai_responses_bridge_drops_a_reasoning_effort_dict_for_a_model_without_reasoning(
openai_responses_route: respx.Route,
):
litellm.completion(
model="openai/responses/gpt-4.1",
messages=[{"role": "user", "content": "hello"}],
reasoning_effort={"effort": "low", "mode": "pro", "context": "current_turn"},
drop_params=True,
api_key="fake-key",
)
request_body: Final = json.loads(openai_responses_route.calls[0].request.content)
assert "reasoning" not in request_body
@pytest.mark.parametrize(
"drop_params, expected_reasoning",
[
(True, {"effort": "low", "context": "current_turn"}),
(
False,
{
"effort": "low",
"exclude": False,
"mode": "turbo",
"context": "current_turn",
"generate_summary": "concise",
},
),
],
)
def test_openai_responses_bridge_drops_reasoning_fields_openai_refuses_only_under_drop_params(
drop_params: bool,
expected_reasoning: dict[str, object],
openai_responses_route: respx.Route,
):
litellm.completion(
model="openai/responses/gpt-6.1-terra",
messages=[{"role": "user", "content": "hello"}],
reasoning_effort={
"effort": "low",
"exclude": False,
"mode": "turbo",
"context": "current_turn",
"generate_summary": "concise",
},
drop_params=drop_params,
api_key="fake-key",
)
request_body: Final = json.loads(openai_responses_route.calls[0].request.content)
assert request_body["reasoning"] == expected_reasoning
@pytest.mark.parametrize(
"declared, drop_params, expected_reasoning",
[
(
{"reasoning_modes": ["standard"], "reasoning_contexts": ["auto", "current_turn"]},
True,
{"effort": "low"},
),
(
{"reasoning_modes": ["pro"], "reasoning_contexts": ["all_turns"]},
True,
{"effort": "low", "mode": "pro", "context": "all_turns"},
),
({}, True, {"effort": "low", "mode": "pro", "context": "all_turns"}),
({"reasoning_modes": ["standard"]}, True, {"effort": "low", "context": "all_turns"}),
(
{"reasoning_modes": ["standard"], "reasoning_contexts": ["auto", "current_turn"]},
False,
{"effort": "low", "mode": "pro", "context": "all_turns"},
),
],
)
def test_openai_responses_bridge_drops_a_mode_or_context_the_model_does_not_list_only_under_drop_params(
declared: dict[str, list[str]],
drop_params: bool,
expected_reasoning: dict[str, str],
openai_responses_route: respx.Route,
):
litellm.register_model(
{
"gpt-6.1-reasoning-test": {
"litellm_provider": "openai",
"mode": "chat",
"supports_reasoning": True,
"input_cost_per_token": 0.0,
"output_cost_per_token": 0.0,
**declared,
}
},
persist_across_reloads=False,
)
litellm.completion(
model="openai/responses/gpt-6.1-reasoning-test",
messages=[{"role": "user", "content": "hello"}],
reasoning_effort={"effort": "low", "mode": "pro", "context": "all_turns"},
drop_params=drop_params,
api_key="fake-key",
)
request_body: Final = json.loads(openai_responses_route.calls[0].request.content)
assert request_body["reasoning"] == expected_reasoning
def test_openai_responses_bridge_forwards_mode_and_context_under_drop_params_for_a_catalog_model_with_no_cost_entry(
openai_responses_route: respx.Route,
):
litellm.open_ai_chat_completion_models.add("unpriced-reasoning-model")
litellm.completion(
model="openai/responses/unpriced-reasoning-model",
messages=[{"role": "user", "content": "hello"}],
reasoning_effort={"effort": "low", "mode": "pro", "context": "all_turns", "generate_summary": "auto"},
drop_params=True,
api_key="fake-key",
)
request_body: Final = json.loads(openai_responses_route.calls[0].request.content)
assert request_body["reasoning"] == {"effort": "low", "mode": "pro", "context": "all_turns"}
def test_openai_responses_bridge_forwards_a_reasoning_effort_dict_whole_for_a_model_outside_the_openai_catalog(
openai_responses_route: respx.Route,
):
reasoning_effort: Final = {"effort": "high", "exclude": True, "budget_tokens": 2000}
litellm.completion(
model="openai/responses/my-proxy-alias",
messages=[{"role": "user", "content": "hello"}],
reasoning_effort=reasoning_effort,
drop_params=True,
api_key="fake-key",
)
request_body: Final = json.loads(openai_responses_route.calls[0].request.content)
assert request_body["reasoning"] == reasoning_effort
@pytest.mark.parametrize(
"additional_drop_params, summary_alias, expected_reasoning",
[
(["reasoning_effort.mode"], {}, {"effort": "low", "context": "current_turn"}),
(
["reasoning_effort.summary"],
{"reasoning_summary": "auto"},
{"effort": "low", "mode": "pro", "context": "current_turn"},
),
(["reasoning_effort"], {"reasoning_summary": "auto"}, None),
("reasoning_effort", {}, {"effort": "low", "mode": "pro", "context": "current_turn"}),
],
)
def test_openai_responses_bridge_applies_additional_drop_params_to_a_reasoning_effort_dict(
additional_drop_params: list[str] | str,
summary_alias: dict[str, str],
expected_reasoning: dict[str, str] | None,
openai_responses_route: respx.Route,
):
litellm.completion(
model="openai/responses/gpt-6.1-terra",
messages=[{"role": "user", "content": "hello"}],
reasoning_effort={"effort": "low", "mode": "pro", "context": "current_turn"},
additional_drop_params=additional_drop_params,
api_key="fake-key",
**summary_alias,
)
request_body: Final = json.loads(openai_responses_route.calls[0].request.content)
assert request_body.get("reasoning") == expected_reasoning
def test_openai_responses_bridge_gives_a_reasoning_effort_dict_the_auto_summary_a_string_gets(
openai_responses_route: respx.Route, monkeypatch: pytest.MonkeyPatch
):
monkeypatch.setenv("LITELLM_REASONING_AUTO_SUMMARY", "true")
litellm.completion(
model="openai/responses/gpt-6.1-terra",
messages=[{"role": "user", "content": "hello"}],
reasoning_effort="low",
api_key="fake-key",
)
litellm.completion(
model="openai/responses/gpt-6.1-terra",
messages=[{"role": "user", "content": "hello"}],
reasoning_effort={"effort": "low", "mode": "pro", "context": "current_turn"},
api_key="fake-key",
)
string_reasoning, dict_reasoning = (
json.loads(call.request.content)["reasoning"] for call in openai_responses_route.calls
)
assert string_reasoning["summary"] is not None
assert dict_reasoning == {
"effort": "low",
"mode": "pro",
"context": "current_turn",
"summary": string_reasoning["summary"],
}
_BRIDGED_RESPONSE_ANSWER: Final = json.dumps(
{
"id": "resp_slow",

View file

@ -115,6 +115,23 @@ def test_github_copilot_backup_rows_match_the_main_catalog() -> None:
assert _github_copilot_catalog_rows(PRICES_PATH) == _github_copilot_catalog_rows(BACKUP_PRICES_PATH)
class _TokenLimits(BaseModel):
model_config = ConfigDict(frozen=True, extra="ignore")
max_input_tokens: int
max_output_tokens: int
@pytest.mark.parametrize("path", (PRICES_PATH, BACKUP_PRICES_PATH), ids=("main", "backup"))
def test_zerogpu_rows_have_an_output_limit_that_fits_the_context_window(path: Path) -> None:
prices: Final[dict[str, JsonValue]] = TypeAdapter(dict[str, JsonValue]).validate_json(path.read_bytes())
rows: Final = MappingProxyType(
{key: _TokenLimits.model_validate(value) for key, value in prices.items() if key.startswith("zerogpu/")}
)
assert rows, f"No ZeroGPU rows found in {path}"
assert tuple(key for key, row in rows.items() if row.max_output_tokens > row.max_input_tokens) == ()
def test_github_copilot_rows_resolve_through_get_model_info() -> None:
rows: Final[Mapping[str, _CopilotEndpointRow]] = _copilot_endpoint_rows(PRICES_PATH)
assert {

View file

@ -20630,6 +20630,37 @@ def test_resolved_litellm_models_answers_through_every_channel_a_request_uses(
assert set(_resolution_router().resolved_litellm_models(model_name)) == set(expected)
def test_resolved_litellm_models_base_model_precedence_is_opt_in() -> None:
router: Final = Router(
model_list=[
{
"model_name": "pooled",
"litellm_params": {"model": "openai/deployment-a", "api_key": "sk-test", "base_model": "gpt-5.5"},
"model_info": {"base_model": "gpt-6-astra"},
},
{
"model_name": "pooled",
"litellm_params": {"model": "openai/deployment-b", "api_key": "sk-test", "base_model": "gpt-5.6-sol"},
},
{
"model_name": "team-deployment",
"litellm_params": {"model": "openai/deployment-c", "api_key": "sk-test"},
"model_info": {
"base_model": "gpt-6-astra",
"team_id": "test-team",
"team_public_model_name": "team-model",
},
},
],
model_group_alias={"nickname": "pooled"},
)
assert router.resolved_litellm_models("nickname") == ("openai/deployment-a", "openai/deployment-b")
assert router.resolved_litellm_models("nickname", use_base_model=True) == ("gpt-6-astra", "gpt-5.6-sol")
assert router.resolved_litellm_models("team-model", "test-team") == ("openai/deployment-c",)
assert router.resolved_litellm_models("team-model", "test-team", use_base_model=True) == ("gpt-6-astra",)
assert router.resolved_litellm_models("never-configured", use_base_model=True) == ()
class TestTierParamsTheTargetAccepts:
"""A tier's litellm_params are applied to every request that tier routes, so one the target
cannot take raised UnsupportedParamsError before the request left the proxy, turning the whole

View file

@ -1195,6 +1195,8 @@ _COST_MAP_ROW_SCHEMA: Final = {
"type": "array",
"items": {"type": "string", "enum": ["none", "minimal", "low", "medium", "high", "xhigh", "max"]},
},
"reasoning_modes": {"type": "array", "items": {"type": "string"}},
"reasoning_contexts": {"type": "array", "items": {"type": "string"}},
"default_reasoning_effort": {
"type": "string",
"enum": ["none", "minimal", "low", "medium", "high", "xhigh"],

View file

@ -224,6 +224,7 @@ describe("provider_info_helpers", () => {
Providers.Sail,
Providers.WANDB,
Providers.ZAI,
Providers.ZeroGPU,
];
const logolessProviders = Object.values(Providers).filter((provider) => !providerLogoMap[provider]);
expect([...logolessProviders].sort()).toEqual([...knownLogolessProviders].sort());
@ -357,6 +358,10 @@ describe("provider_info_helpers", () => {
expect(getPlaceholder(Providers.Cognition)).toBe("cognition/swe-1.7");
});
it("should return zerogpu/gpt-oss-120b placeholder for ZeroGPU provider", () => {
expect(getPlaceholder("ZeroGPU")).toBe("zerogpu/gpt-oss-120b");
});
it("should return a chatgpt/ placeholder for the CHATGPT dropdown key", () => {
expect(getPlaceholder("CHATGPT")).toBe("chatgpt/gpt-5.4");
});

View file

@ -197,6 +197,7 @@ export enum Providers {
xAI = "xAI",
XINFERENCE = "Xinference",
ZAI = "Z.AI (Zhipu AI)",
ZeroGPU = "ZeroGPU",
}
export const provider_map: Record<string, string> = {
@ -325,6 +326,7 @@ export const provider_map: Record<string, string> = {
xAI: "xai",
XINFERENCE: "xinference",
ZAI: "zai",
ZeroGPU: "zerogpu",
};
const standaloneSubproviderSlugs = new Set<string>(["bedrock_mantle"]);
@ -497,6 +499,7 @@ const providerPlaceholderMap: Partial<Record<Providers, string>> = {
[Providers.Voyage]: "voyage/",
[Providers.WATSONX]: "watsonx/ibm/granite-3-3-8b-instruct",
[Providers.ZAI]: "zai/glm-4.5",
[Providers.ZeroGPU]: "zerogpu/gpt-oss-120b",
};
export const getPlaceholder = (selectedProvider: string): string => {