From 4de7a7443ac5506f422efb36e96387aaae185607 Mon Sep 17 00:00:00 2001 From: mateo Date: Fri, 7 Aug 2026 19:27:50 +0000 Subject: [PATCH 1/3] refactor(types): declare mirrored pricing fields on ModelInfo Co-Authored-By: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com> --- litellm/types/router.py | 18 +++--- litellm/types/utils.py | 21 +++++-- tests/test_litellm/types/test_router.py | 73 +++++++++++++++++++++++++ 3 files changed, 96 insertions(+), 16 deletions(-) create mode 100644 tests/test_litellm/types/test_router.py diff --git a/litellm/types/router.py b/litellm/types/router.py index 8b4b547bdcc..487d95cd762 100644 --- a/litellm/types/router.py +++ b/litellm/types/router.py @@ -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: Final = tuple(MirroredPricingParams.model_fields) class Deployment(BaseModel): diff --git a/litellm/types/utils.py b/litellm/types/utils.py index 35d4250782f..18cf9461648 100644 --- a/litellm/types/utils.py +++ b/litellm/types/utils.py @@ -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 diff --git a/tests/test_litellm/types/test_router.py b/tests/test_litellm/types/test_router.py new file mode 100644 index 00000000000..1b66863a82f --- /dev/null +++ b/tests/test_litellm/types/test_router.py @@ -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") From 24ac999cf7f74d7b7495d07f7f7783d691877e50 Mon Sep 17 00:00:00 2001 From: mateo Date: Fri, 7 Aug 2026 20:06:41 +0000 Subject: [PATCH 2/3] fix(types): drop Final on SPECIAL_MODEL_INFO_PARAMS for star-import rebinding Co-Authored-By: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com> --- litellm/types/router.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/litellm/types/router.py b/litellm/types/router.py index 487d95cd762..4280da08cbb 100644 --- a/litellm/types/router.py +++ b/litellm/types/router.py @@ -429,7 +429,7 @@ class DeploymentTypedDict(TypedDict, total=False): model_info: dict -SPECIAL_MODEL_INFO_PARAMS: Final = tuple(MirroredPricingParams.model_fields) +SPECIAL_MODEL_INFO_PARAMS = tuple(MirroredPricingParams.model_fields) class Deployment(BaseModel): From f668c1060981cd7698fb5946ab2bc708dc0f59a6 Mon Sep 17 00:00:00 2001 From: mateo Date: Fri, 7 Aug 2026 20:15:39 +0000 Subject: [PATCH 3/3] chore(ui): regenerate dashboard api types for ModelInfo pricing fields Co-Authored-By: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com> --- ui/litellm-dashboard/src/lib/http/schema.d.ts | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/ui/litellm-dashboard/src/lib/http/schema.d.ts b/ui/litellm-dashboard/src/lib/http/schema.d.ts index 8e950874a10..a75c23da1cf 100644 --- a/ui/litellm-dashboard/src/lib/http/schema.d.ts +++ b/ui/litellm-dashboard/src/lib/http/schema.d.ts @@ -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 */