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())