Merge pull request #36215 from BerriAI/litellm_declare_model_info_pricing

refactor(types): declare mirrored pricing fields on ModelInfo
This commit is contained in:
Mateo Wang 2026-08-07 21:06:14 -07:00 • committed by GitHub
commit d0758a291c
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
4 changed files with 108 additions and 16 deletions

View file

@ -17,7 +17,12 @@ from .completion import CompletionRequest
from .embedding import EmbeddingRequest
from .llms.openai import OpenAIFileObject
from .search import SearchProvider
from .utils import CustomPricingLiteLLMParams, ModelResponse, StandardLoggingRoutingDecision
from .utils import (
CustomPricingLiteLLMParams,
MirroredPricingParams,
ModelResponse,
StandardLoggingRoutingDecision,
)
class ConfigurableClientsideParamsCustomAuth(TypedDict):
@ -122,7 +127,7 @@ class UpdateRouterConfig(BaseModel):
model_config = ConfigDict(protected_namespaces=())
class ModelInfo(BaseModel):
class ModelInfo(MirroredPricingParams):
id: str | None # Allow id to be optional on input, but it will always be present as a str in the model instance
db_model: bool = False # used for proxy - to separate models which are stored in the db vs. config.
updated_at: datetime.datetime | None = None
@ -424,14 +429,7 @@ class DeploymentTypedDict(TypedDict, total=False):
model_info: dict
SPECIAL_MODEL_INFO_PARAMS = [
"input_cost_per_token",
"output_cost_per_token",
"input_cost_per_character",
"output_cost_per_character",
"cache_read_input_token_cost",
"cache_creation_input_token_cost",
]
SPECIAL_MODEL_INFO_PARAMS = tuple(MirroredPricingParams.model_fields)
class Deployment(BaseModel):

View file

@ -3245,10 +3245,23 @@ class StandardCallbackDynamicParams(TypedDict, total=False):
litellm_disabled_callbacks: list[str] | None
class CustomPricingLiteLLMParams(BaseModel):
## CUSTOM PRICING ##
class MirroredPricingParams(BaseModel):
"""Pricing overrides that ``Deployment.__init__`` mirrors from ``litellm_params``
onto ``model_info``, so both blobs hold the same rate.
Declared once and inherited by both sides of that mirror, so the two can't drift.
"""
input_cost_per_token: float | None = None
output_cost_per_token: float | None = None
input_cost_per_character: float | None = None
output_cost_per_character: float | None = None
cache_read_input_token_cost: float | None = None
cache_creation_input_token_cost: float | None = None
class CustomPricingLiteLLMParams(MirroredPricingParams):
## CUSTOM PRICING ##
input_cost_per_second: float | None = None
output_cost_per_second: float | None = None
output_cost_per_second_1080p: float | None = None
@ -3259,7 +3272,6 @@ class CustomPricingLiteLLMParams(BaseModel):
# This allows any model_info parameter to be set in litellm_params
input_cost_per_token_flex: float | None = None
input_cost_per_token_priority: float | None = None
cache_creation_input_token_cost: float | None = None
cache_creation_input_token_cost_above_1hr: float | None = None
cache_creation_input_token_cost_above_200k_tokens: float | None = None
cache_creation_input_token_cost_above_272k_tokens: float | None = None
@ -3268,7 +3280,6 @@ class CustomPricingLiteLLMParams(BaseModel):
cache_creation_input_token_cost_flex: float | None = None
cache_creation_input_token_cost_priority: float | None = None
cache_creation_input_audio_token_cost: float | None = None
cache_read_input_token_cost: float | None = None
cache_read_input_token_cost_flex: float | None = None
cache_read_input_token_cost_priority: float | None = None
cache_read_input_token_cost_above_200k_tokens: float | None = None
@ -3276,7 +3287,6 @@ class CustomPricingLiteLLMParams(BaseModel):
cache_read_input_token_cost_above_272k_tokens_priority: float | None = None
cache_read_input_token_cost_above_272k_tokens_flex: float | None = None
cache_read_input_audio_token_cost: float | None = None
input_cost_per_character: float | None = None
input_cost_per_character_above_128k_tokens: float | None = None
input_cost_per_audio_token: float | None = None
input_cost_per_token_cache_hit: float | None = None
@ -3298,7 +3308,6 @@ class CustomPricingLiteLLMParams(BaseModel):
output_cost_per_token_batches: float | None = None
output_cost_per_token_flex: float | None = None
output_cost_per_token_priority: float | None = None
output_cost_per_character: float | None = None
output_cost_per_audio_token: float | None = None
output_cost_per_token_above_128k_tokens: float | None = None
output_cost_per_token_above_200k_tokens: float | None = None

View file

@ -0,0 +1,73 @@
import pytest
from litellm.types.router import (
SPECIAL_MODEL_INFO_PARAMS,
Deployment,
LiteLLM_Params,
ModelInfo,
)
from litellm.types.utils import CustomPricingLiteLLMParams, MirroredPricingParams
def test_model_info_declares_mirrored_pricing_fields():
"""The pricing keys Deployment mirrors onto model_info must be declared fields, not
extras that only survive because ModelInfo sets extra="allow"."""
for field in SPECIAL_MODEL_INFO_PARAMS:
assert field in ModelInfo.model_fields
info = ModelInfo(id="x", input_cost_per_token=1e-06)
assert info.__pydantic_extra__ == {}
assert info.input_cost_per_token == 1e-06
def test_special_model_info_params_cannot_drift_from_the_mirror():
assert SPECIAL_MODEL_INFO_PARAMS == tuple(MirroredPricingParams.model_fields)
assert set(SPECIAL_MODEL_INFO_PARAMS) <= set(CustomPricingLiteLLMParams.model_fields)
assert set(SPECIAL_MODEL_INFO_PARAMS) <= set(LiteLLM_Params.model_fields)
def test_custom_pricing_params_keeps_every_field_it_had():
"""The mirrored fields moved to a base class; none of them may go missing from
CustomPricingLiteLLMParams, whose model_fields drive custom-pricing detection."""
for field in (
"input_cost_per_token",
"output_cost_per_token",
"input_cost_per_character",
"output_cost_per_character",
"cache_read_input_token_cost",
"cache_creation_input_token_cost",
"input_cost_per_second",
"cache_read_input_token_cost_flex",
"input_cost_per_character_above_128k_tokens",
"output_cost_per_audio_token",
):
assert field in CustomPricingLiteLLMParams.model_fields
@pytest.mark.parametrize("field", SPECIAL_MODEL_INFO_PARAMS)
def test_deployment_mirrors_pricing_from_litellm_params_onto_model_info(field):
deployment = Deployment(
model_name="my-model",
litellm_params=LiteLLM_Params(model="gpt-4o", **{field: 3e-06}),
)
assert getattr(deployment.model_info, field) == 3e-06
assert deployment.model_info.model_dump(exclude_none=True)[field] == 3e-06
def test_unset_pricing_is_still_absent_from_dumps():
"""/model/info responses and DB writes dump model_info with exclude_none=True, so
declaring the pricing fields must not start emitting ~6 null keys per deployment."""
dumped = ModelInfo(id="x").model_dump(exclude_none=True)
assert [field for field in SPECIAL_MODEL_INFO_PARAMS if field in dumped] == []
def test_pricing_strings_are_coerced_to_float():
"""Cost values arrive from the DB and the Admin UI as strings; they must land as
floats so cost calculation doesn't multiply a str."""
info = ModelInfo(id="x", output_cost_per_token="0.000002")
assert info.output_cost_per_token == 2e-06
def test_invalid_pricing_is_rejected():
with pytest.raises(ValueError):
ModelInfo(id="x", input_cost_per_token="free")

View file

@ -35294,6 +35294,10 @@ export interface components {
base_model?: string | null;
/** Blocked */
blocked?: boolean | null;
/** Cache Creation Input Token Cost */
cache_creation_input_token_cost?: number | null;
/** Cache Read Input Token Cost */
cache_read_input_token_cost?: number | null;
/** Created At */
created_at?: string | null;
/** Created By */
@ -35305,6 +35309,14 @@ export interface components {
db_model: boolean;
/** Id */
id: string | null;
/** Input Cost Per Character */
input_cost_per_character?: number | null;
/** Input Cost Per Token */
input_cost_per_token?: number | null;
/** Output Cost Per Character */
output_cost_per_character?: number | null;
/** Output Cost Per Token */
output_cost_per_token?: number | null;
/** Team Id */
team_id?: string | null;
/** Team Public Model Name */