mirror of
https://github.com/BerriAI/litellm.git
synced 2026-10-01 02:02:20 +00:00
A window rule may carry override_dates, YYYY-MM-DD dates on which it alone decides, so the generated schema, the OffPeakWindow TypedDict, and the Rust model catalog now model it.
422 lines
17 KiB
Python
422 lines
17 KiB
Python
from __future__ import annotations
|
|
|
|
import json
|
|
import sys
|
|
from pathlib import Path
|
|
from typing import Final, 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"}
|
|
TIME_WINDOW: Final[JsonSchema] = {"type": "string", "pattern": r"^([01]\d|2[0-3]):[0-5]\d-([01]\d|2[0-3]):[0-5]\d$"}
|
|
ISO_DATE: Final[JsonSchema] = {
|
|
"type": "string",
|
|
"format": "date",
|
|
"pattern": "^\\d{4}-(0[1-9]|1[0-2])-(0[1-9]|[12]\\d|3[01])$",
|
|
}
|
|
WEEKDAY_PATTERN: Final = (
|
|
r"(?i)^(mon|monday|tue|tues|tuesday|wed|wednesday|thu|thur|thurs|thursday|fri|friday|sat|saturday|sun|sunday)$"
|
|
)
|
|
|
|
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",
|
|
}
|
|
)
|
|
|
|
HOURS_UTC: Final[JsonSchema] = {
|
|
"description": 'UTC "HH:MM-HH:MM" window, or a list of them; a window may wrap past midnight.',
|
|
"oneOf": [TIME_WINDOW, {"type": "array", "items": TIME_WINDOW, "minItems": 1}],
|
|
}
|
|
|
|
OFF_PEAK_WINDOW: Final[JsonSchema] = {
|
|
"type": "object",
|
|
"properties": {
|
|
"hours_utc": HOURS_UTC,
|
|
"weekdays": {
|
|
"type": "array",
|
|
"description": "ISO-8601 weekday numbers (1 = Monday .. 7 = Sunday) or English day names the window applies on.",
|
|
"items": {
|
|
"oneOf": [
|
|
{"type": "integer", "minimum": 1, "maximum": 7},
|
|
{"type": "string", "pattern": WEEKDAY_PATTERN},
|
|
]
|
|
},
|
|
"minItems": 1,
|
|
},
|
|
"override_dates": {
|
|
"type": "array",
|
|
"items": ISO_DATE,
|
|
"minItems": 1,
|
|
"description": (
|
|
"YYYY-MM-DD dates, read on weekday_timezone, on which this rule alone decides: "
|
|
"weekdays and every other window are ignored. On any other date the rule does not "
|
|
"apply, and malformed entries disable it."
|
|
),
|
|
},
|
|
},
|
|
"required": ["hours_utc"],
|
|
"additionalProperties": False,
|
|
}
|
|
|
|
OBJECT_KEYS: dict[str, JsonSchema] = {
|
|
"off_peak_pricing": {
|
|
"type": "object",
|
|
"description": "Rates that replace the same-named base fields while the request falls inside the stated UTC windows.",
|
|
"properties": {
|
|
"hours_utc": HOURS_UTC,
|
|
"windows": {"type": "array", "items": OFF_PEAK_WINDOW, "minItems": 1},
|
|
"weekday_timezone": {
|
|
"type": "string",
|
|
"description": "IANA zone the weekdays of each window are read on; defaults to UTC.",
|
|
},
|
|
"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,
|
|
},
|
|
"anyOf": [{"required": ["hours_utc"]}, {"required": ["windows"]}],
|
|
"additionalProperties": False,
|
|
},
|
|
"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_audio_formats": {
|
|
"type": "array",
|
|
"description": "Audio container formats the model can return.",
|
|
"items": {"type": "string", "enum": ["mp3", "wav"]},
|
|
},
|
|
"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": {
|
|
**ISO_DATE,
|
|
"description": "Date the provider deprecates the model, YYYY-MM-DD.",
|
|
},
|
|
"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,
|
|
"vertex_ai_audio_api": {
|
|
"type": "string",
|
|
"enum": ["lyria_predict", "lyria_interactions"],
|
|
},
|
|
}
|
|
|
|
|
|
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())
|