mirror of
https://github.com/BerriAI/litellm.git
synced 2026-09-08 22:21:35 +00:00
A gpt-5 model accepts a non-default temperature only while its effective reasoning effort resolves to "none". litellm had no representation of the effort a model applies when the request omits reasoning_effort, so it substituted supports_none_reasoning_effort, which is a different fact. Every model that supports "none" without defaulting to it therefore had temperature forwarded and rejected upstream, and because the carve-out returned before the drop_params branch, drop_params: true could not save it. Declare the fact instead. A new cost-map key, default_reasoning_effort, states the effort the provider applies when the request omits one, and one shared predicate resolves the effective effort from it: an explicit reasoning_effort wins, otherwise the declared default, otherwise the catalogue decides. That last step matters because the cost map is fetched from the published branch at import time, so it can be OLDER than the code reading it. On such a map every model looks undeclared, and reading that as "reasoning is active" would strip temperature from the 39 gpt-5.1/5.2/5.4 entries that accept it, a regression caused by data lag rather than by anything about the model. So an absent declaration is only meaningful once the catalogue carries the key at all; a map that predates the feature keeps the answer litellm gave before it existed, and the conservative answer applies from the moment the data lands. The top_p/logprobs/top_logprobs gate carried the same assumption spelled differently and now shares the predicate, as does the Responses API, which reimplemented the rule and is what the default /v1/messages bridge routes openai models through. Azure normalises its routing names in one resolver that every capability lookup goes through, which replaces its bespoke per-lookup rewrite. Declared on the 37 gpt-5.1/5.2/5.4 entries measured to accept temperature=0 today, so their behaviour is unchanged. The 23 gpt-5.5/5.6 entries that reject it stay undeclared and are fixed once the catalogue carries the key. Resolves LIT-3797 Resolves LIT-5028
354 lines
14 KiB
Python
354 lines
14 KiB
Python
from __future__ import annotations
|
|
|
|
import json
|
|
import sys
|
|
from pathlib import Path
|
|
from typing import Optional
|
|
|
|
import jsonschema
|
|
|
|
REPO_ROOT = Path(__file__).parent.parent
|
|
PRICES_PATH = REPO_ROOT / "model_prices_and_context_window.json"
|
|
SCHEMA_PATH = REPO_ROOT / "model_prices_and_context_window.schema.json"
|
|
|
|
SPECIAL_ROOT_KEYS = frozenset({"sample_spec", "fallback_generalizations"})
|
|
|
|
JsonSchema = dict
|
|
|
|
NONNEG_NUMBER: JsonSchema = {"type": "number", "minimum": 0}
|
|
NONNEG_INTEGER: JsonSchema = {"type": "integer", "minimum": 0}
|
|
BOOLEAN: JsonSchema = {"type": "boolean"}
|
|
STRING: JsonSchema = {"type": "string"}
|
|
|
|
EXTRA_BOOLEAN_KEYS = frozenset(
|
|
{
|
|
"gemini_native_audio",
|
|
"gemini_audio_only_live",
|
|
"uses_embed_content",
|
|
"use_openai_responses_path",
|
|
"bedrock_converse_supports_strict_tools",
|
|
"thinking_always_on",
|
|
}
|
|
)
|
|
|
|
OBJECT_KEYS: dict[str, JsonSchema] = {
|
|
"search_context_cost_per_query": {
|
|
"type": "object",
|
|
"description": "USD cost per web search query, keyed by search context size.",
|
|
"properties": {
|
|
"search_context_size_low": NONNEG_NUMBER,
|
|
"search_context_size_medium": NONNEG_NUMBER,
|
|
"search_context_size_high": NONNEG_NUMBER,
|
|
},
|
|
"additionalProperties": False,
|
|
},
|
|
"guardrail_cost_per_unit": {
|
|
"type": "object",
|
|
"description": "USD cost per billable guardrail unit, keyed by the provider's usage counter name (e.g. Bedrock's contentPolicyUnits).",
|
|
"additionalProperties": NONNEG_NUMBER,
|
|
},
|
|
"metadata": {
|
|
"type": "object",
|
|
"description": "Free-form notes about the entry (e.g. pricing derivation).",
|
|
},
|
|
"provider_specific_entry": {
|
|
"type": "object",
|
|
"description": "Provider-internal routing hints (e.g. bedrock_invocation_schema).",
|
|
},
|
|
}
|
|
|
|
ARRAY_KEYS: dict[str, JsonSchema] = {
|
|
"supported_endpoints": {
|
|
"type": "array",
|
|
"description": "OpenAI-style API routes this model can be called through, e.g. /v1/chat/completions.",
|
|
"items": STRING,
|
|
},
|
|
"supported_modalities": {
|
|
"type": "array",
|
|
"description": "Input modalities the model accepts.",
|
|
"items": {"type": "string", "enum": ["text", "image", "audio", "video"]},
|
|
},
|
|
"supported_output_modalities": {
|
|
"type": "array",
|
|
"description": "Output modalities the model can produce.",
|
|
"items": {"type": "string", "enum": ["text", "image", "audio", "video", "code"]},
|
|
},
|
|
"reasoning_effort_levels": {
|
|
"type": "array",
|
|
"description": "Exact reasoning_effort levels this deployment accepts; wins over supports_* flags.",
|
|
"items": {"type": "string", "enum": ["none", "minimal", "low", "medium", "high", "xhigh", "max"]},
|
|
},
|
|
"supported_regions": {
|
|
"type": "array",
|
|
"description": "Cloud regions the model is available in ('global' or region ids).",
|
|
"items": STRING,
|
|
},
|
|
"tiered_pricing": {
|
|
"type": "array",
|
|
"description": "Context-length or result-count tiered rates; each tier's costs apply within its range.",
|
|
"items": {
|
|
"type": "object",
|
|
"properties": {
|
|
"range": {
|
|
"type": "array",
|
|
"description": "[min, max] prompt-token span this tier applies to.",
|
|
"items": NONNEG_NUMBER,
|
|
"minItems": 2,
|
|
"maxItems": 2,
|
|
},
|
|
"max_results_range": {
|
|
"type": "array",
|
|
"description": "[min, max] result-count span this tier applies to (search models).",
|
|
"items": NONNEG_NUMBER,
|
|
"minItems": 2,
|
|
"maxItems": 2,
|
|
},
|
|
"input_cost_per_token": NONNEG_NUMBER,
|
|
"output_cost_per_token": NONNEG_NUMBER,
|
|
"output_cost_per_reasoning_token": NONNEG_NUMBER,
|
|
"cache_read_input_token_cost": NONNEG_NUMBER,
|
|
"cache_creation_input_token_cost": NONNEG_NUMBER,
|
|
"input_cost_per_query": NONNEG_NUMBER,
|
|
},
|
|
"additionalProperties": False,
|
|
},
|
|
},
|
|
}
|
|
|
|
INTEGER_KEYS: dict[str, JsonSchema] = {
|
|
"max_tokens": {
|
|
**NONNEG_INTEGER,
|
|
"description": "Legacy field: max output tokens if the provider specifies it, else max input tokens.",
|
|
},
|
|
"max_input_tokens": {
|
|
**NONNEG_INTEGER,
|
|
"description": "Maximum prompt/context tokens the model accepts.",
|
|
},
|
|
"max_output_tokens": {
|
|
**NONNEG_INTEGER,
|
|
"description": "Maximum tokens the model can generate in one response.",
|
|
},
|
|
"output_vector_size": {
|
|
**NONNEG_INTEGER,
|
|
"description": "Embedding dimension for embedding models.",
|
|
},
|
|
"prompt_cache_min_tokens": {
|
|
**NONNEG_INTEGER,
|
|
"description": "Smallest prefix the provider will actually cache; absent means the provider default applies.",
|
|
},
|
|
"tpm": {**NONNEG_INTEGER, "description": "Provider default tokens-per-minute limit."},
|
|
"rpm": {**NONNEG_INTEGER, "description": "Provider default requests-per-minute limit."},
|
|
}
|
|
|
|
NUMBER_KEYS: dict[str, JsonSchema] = {
|
|
"regional_processing_uplift_multiplier_eu": {
|
|
"type": "number",
|
|
"minimum": 1,
|
|
"description": "Multiplier applied to all token costs for EU data residency (e.g. 1.10 = +10%).",
|
|
},
|
|
"regional_processing_uplift_multiplier_us": {
|
|
"type": "number",
|
|
"minimum": 1,
|
|
"description": "Multiplier applied to all token costs for US data residency (e.g. 1.10 = +10%).",
|
|
},
|
|
"regional_endpoint_uplift_multiplier": {
|
|
"type": "number",
|
|
"minimum": 1,
|
|
"description": "Multiplier applied to all token costs when served from a non-global Vertex AI endpoint (e.g. 1.10 = +10%).",
|
|
},
|
|
}
|
|
|
|
COST_DESCRIPTIONS: dict[str, str] = {
|
|
"input_cost_per_token": "USD per prompt token.",
|
|
"output_cost_per_token": "USD per generated token.",
|
|
"output_cost_per_reasoning_token": "USD per reasoning/thinking token, when billed separately.",
|
|
"google_maps_grounding_cost_per_query": (
|
|
"USD per Grounding with Google Maps request; billed per query or per prompt per web_search_billing_unit."
|
|
),
|
|
"cache_creation_input_token_cost": "USD per token written to the provider's prompt cache.",
|
|
"cache_read_input_token_cost": "USD per prompt token served from the provider's prompt cache.",
|
|
"input_cost_per_token_batches": "USD per prompt token via the provider's batch API.",
|
|
"output_cost_per_token_batches": "USD per generated token via the provider's batch API.",
|
|
}
|
|
|
|
|
|
def cost_description(key: str) -> Optional[str]:
|
|
if key in COST_DESCRIPTIONS:
|
|
return COST_DESCRIPTIONS[key]
|
|
if key.endswith("_flex"):
|
|
return "Flex service-tier rate for the same-named base field."
|
|
if key.endswith("_priority"):
|
|
return "Priority service-tier rate for the same-named base field."
|
|
if "_above_" in key:
|
|
return "Rate applied once the prompt exceeds the token threshold in the field name."
|
|
return None
|
|
|
|
|
|
def cost_schema(key: str) -> JsonSchema:
|
|
description = cost_description(key)
|
|
return {**NONNEG_NUMBER, "description": description} if description else dict(NONNEG_NUMBER)
|
|
|
|
|
|
def string_key_schemas(modes: tuple) -> dict[str, JsonSchema]:
|
|
return {
|
|
"litellm_provider": {
|
|
"type": "string",
|
|
"description": "LiteLLM provider slug; one of https://docs.litellm.ai/docs/providers.",
|
|
},
|
|
"mode": {
|
|
"type": "string",
|
|
"description": "Primary API surface / task type of the model.",
|
|
"enum": list(modes),
|
|
},
|
|
"source": {
|
|
"type": "string",
|
|
"description": "URL of the provider pricing/model page this entry was taken from.",
|
|
},
|
|
"deprecation_date": {
|
|
"type": "string",
|
|
"description": "Date the provider deprecates the model, YYYY-MM-DD.",
|
|
"format": "date",
|
|
"pattern": "^\\d{4}-(0[1-9]|1[0-2])-(0[1-9]|[12]\\d|3[01])$",
|
|
},
|
|
"web_search_billing_unit": {
|
|
"type": "string",
|
|
"description": "Whether web search is billed per query or per prompt.",
|
|
"enum": ["per_query", "per_prompt"],
|
|
},
|
|
"bedrock_output_config_effort_ceiling": {
|
|
"type": "string",
|
|
"description": "Highest reasoning effort the Bedrock output_config accepts for this model.",
|
|
"enum": ["low", "medium", "high", "max", "xhigh"],
|
|
},
|
|
"default_reasoning_effort": {
|
|
"type": "string",
|
|
"description": (
|
|
"Reasoning effort the provider applies when the request omits reasoning_effort. "
|
|
"Gates whether a non-default temperature or the top_p/logprobs sampling params are "
|
|
"accepted, which hold only when the effort resolves to 'none'."
|
|
),
|
|
"enum": ["none", "minimal", "low", "medium", "high", "xhigh"],
|
|
},
|
|
"comment": STRING,
|
|
"audio_transcription_config": STRING,
|
|
}
|
|
|
|
|
|
def classify(key: str, modes: tuple) -> Optional[JsonSchema]:
|
|
curated = {**OBJECT_KEYS, **ARRAY_KEYS, **string_key_schemas(modes), **INTEGER_KEYS, **NUMBER_KEYS}
|
|
if key in curated:
|
|
return curated[key]
|
|
if key.startswith("supports_") or key in EXTRA_BOOLEAN_KEYS:
|
|
return BOOLEAN
|
|
if "cost" in key:
|
|
return cost_schema(key)
|
|
return None
|
|
|
|
|
|
def build_schema(prices: dict) -> JsonSchema:
|
|
entries = {name: entry for name, entry in prices.items() if name not in SPECIAL_ROOT_KEYS}
|
|
all_keys = tuple(sorted({key for entry in entries.values() for key in entry}))
|
|
modes = tuple(sorted({entry["mode"] for entry in entries.values() if "mode" in entry}))
|
|
unclassified = tuple(key for key in all_keys if classify(key, modes) is None)
|
|
if unclassified:
|
|
raise SystemExit(
|
|
f"Unclassified keys in {PRICES_PATH.name}: {', '.join(unclassified)}. "
|
|
f"Add them to the key tables in {Path(__file__).name} and rerun it."
|
|
)
|
|
entry_properties = {key: classify(key, modes) for key in all_keys}
|
|
return {
|
|
"$schema": "https://json-schema.org/draft/2020-12/schema",
|
|
"title": "LiteLLM model_prices_and_context_window.json",
|
|
"description": (
|
|
"Schema for LiteLLM's model price and context window registry "
|
|
"(https://github.com/BerriAI/litellm/blob/main/model_prices_and_context_window.json). "
|
|
"Every top-level key except 'sample_spec' and 'fallback_generalizations' is a model id, "
|
|
"optionally prefixed with its provider (e.g. 'azure/gpt-5.4'), mapping to a model entry. "
|
|
"All costs are USD per unit. New optional fields are added regularly, so consumers should "
|
|
"ignore unknown fields rather than reject them."
|
|
),
|
|
"type": "object",
|
|
"properties": {
|
|
"sample_spec": {
|
|
"type": "object",
|
|
"description": (
|
|
"Documentation placeholder illustrating the entry shape; not a real model and not "
|
|
"schema-conformant (several values are prose)."
|
|
),
|
|
},
|
|
"fallback_generalizations": {
|
|
"type": "object",
|
|
"description": "Regex rules that generalize unknown model ids to known families; not a model entry.",
|
|
"properties": {
|
|
"rules": {
|
|
"type": "array",
|
|
"items": {
|
|
"type": "object",
|
|
"properties": {
|
|
"name": STRING,
|
|
"pattern": STRING,
|
|
"description": STRING,
|
|
},
|
|
"required": ["name", "pattern"],
|
|
"additionalProperties": True,
|
|
},
|
|
}
|
|
},
|
|
"additionalProperties": False,
|
|
},
|
|
},
|
|
"additionalProperties": {"$ref": "#/$defs/modelEntry"},
|
|
"$defs": {
|
|
"modelEntry": {
|
|
"type": "object",
|
|
"description": (
|
|
"Pricing, limits, and capability flags for one model. Fields other than litellm_provider "
|
|
"are optional; boolean capability flags are simply omitted when unknown or false."
|
|
),
|
|
"required": ["litellm_provider"],
|
|
"properties": entry_properties,
|
|
"additionalProperties": True,
|
|
}
|
|
},
|
|
}
|
|
|
|
|
|
def render(schema: JsonSchema) -> str:
|
|
return json.dumps(schema, indent=2) + "\n"
|
|
|
|
|
|
def validation_errors(prices: dict, schema: JsonSchema) -> tuple:
|
|
validator = jsonschema.Draft202012Validator(
|
|
schema, format_checker=jsonschema.Draft202012Validator.FORMAT_CHECKER
|
|
)
|
|
return tuple(
|
|
f"{'.'.join(str(part) for part in error.absolute_path)}: {error.message}"
|
|
for error in validator.iter_errors(prices)
|
|
)
|
|
|
|
|
|
def main() -> int:
|
|
check = "--check" in sys.argv[1:]
|
|
prices = json.loads(PRICES_PATH.read_text())
|
|
rendered = render(build_schema(prices))
|
|
errors = validation_errors(prices, json.loads(rendered))
|
|
if errors:
|
|
print(f"{PRICES_PATH.name} does not validate against the generated schema:")
|
|
print("\n".join(errors[:20]))
|
|
return 1
|
|
if not check:
|
|
SCHEMA_PATH.write_text(rendered)
|
|
print(f"wrote {SCHEMA_PATH}")
|
|
return 0
|
|
if not SCHEMA_PATH.exists() or SCHEMA_PATH.read_text() != rendered:
|
|
print(
|
|
f"{SCHEMA_PATH.name} is out of sync with {PRICES_PATH.name}. "
|
|
f"Run `python {Path(__file__).relative_to(REPO_ROOT)}` and commit the result."
|
|
)
|
|
return 1
|
|
print(f"{SCHEMA_PATH.name} is in sync and {PRICES_PATH.name} validates against it")
|
|
return 0
|
|
|
|
|
|
if __name__ == "__main__":
|
|
sys.exit(main())
|