mirror of
https://github.com/BerriAI/litellm.git
synced 2026-11-13 08:52:02 +00:00
Merge commit '0c14826f58' into litellm_silent_experiment_spend_row_flag
This commit is contained in:
commit
054f220e53
32 changed files with 2979 additions and 47 deletions
|
|
@ -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/)
|
||||
|
||||
|
|
|
|||
|
|
@ -113,6 +113,7 @@ const OPENAI_COMPATIBLE_PROVIDERS: &[&str] = &[
|
|||
"meta",
|
||||
"cognition",
|
||||
"scx-ai",
|
||||
"zerogpu",
|
||||
];
|
||||
|
||||
impl ExceptionFamily {
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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",
|
||||
|
|
|
|||
|
|
@ -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"
|
||||
|
||||
|
||||
|
|
|
|||
|
|
@ -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."""
|
||||
|
|
|
|||
|
|
@ -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.
|
||||
|
|
|
|||
|
|
@ -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",
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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},
|
||||
|
|
|
|||
|
|
@ -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",
|
||||
|
|
|
|||
|
|
@ -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:
|
||||
|
|
|
|||
|
|
@ -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",
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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 ()
|
||||
|
|
|
|||
|
|
@ -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"
|
||||
|
|
|
|||
|
|
@ -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),
|
||||
|
|
|
|||
|
|
@ -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},
|
||||
|
|
|
|||
|
|
@ -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",
|
||||
|
|
|
|||
|
|
@ -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 == ()
|
||||
|
|
|
|||
|
|
@ -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"""
|
||||
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
239
tests/unit/llms/openai_like/test_zerogpu_provider.py
Normal file
239
tests/unit/llms/openai_like/test_zerogpu_provider.py
Normal 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
|
||||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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",
|
||||
|
|
|
|||
|
|
@ -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 {
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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"],
|
||||
|
|
|
|||
|
|
@ -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");
|
||||
});
|
||||
|
|
|
|||
|
|
@ -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 => {
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue