fix(types): break the CachedTokensDetails import cycle

CodeQL flagged two module-level cyclic imports introduced by defining
CachedTokensDetails in litellm.types.llms.openai and importing it from
litellm.types.utils and litellm.cost_calculator. The class now lives in
litellm.types.llms.base, which imports nothing from litellm, and every
user imports it from there.

Also pins that combining realtime usages where only one response.done
carries cached_tokens_details keeps the earlier modality split in both
orders, and commits the regenerated dashboard API types.
This commit is contained in:
mateo-berri 2026-09-13 01:14:35 -07:00
parent a28e595a9d
commit 76ae35dfcd
7 changed files with 53 additions and 12 deletions

View file

@ -97,6 +97,7 @@ from litellm.llms.vertex_ai.cost_calculator import cost_router as google_cost_ro
from litellm.llms.xai.cost_calculator import cost_per_token as xai_cost_per_token from litellm.llms.xai.cost_calculator import cost_per_token as xai_cost_per_token
from litellm.responses.utils import ResponseAPILoggingUtils from litellm.responses.utils import ResponseAPILoggingUtils
from litellm.types.agents import LiteLLMSendMessageResponse from litellm.types.agents import LiteLLMSendMessageResponse
from litellm.types.llms.base import CachedTokensDetails
from litellm.types.llms.openai import ( from litellm.types.llms.openai import (
HttpxBinaryResponseContent, HttpxBinaryResponseContent,
ImageGenerationRequestQuality, ImageGenerationRequestQuality,
@ -109,7 +110,6 @@ from litellm.types.llms.openai import (
) )
from litellm.types.rerank import RerankBilledUnits, RerankResponse from litellm.types.rerank import RerankBilledUnits, RerankResponse
from litellm.types.utils import ( from litellm.types.utils import (
CachedTokensDetails,
CallTypesLiteral, CallTypesLiteral,
LiteLLMRealtimeStreamLoggingObject, LiteLLMRealtimeStreamLoggingObject,
LlmProviders, LlmProviders,

View file

@ -43,9 +43,9 @@ from litellm.litellm_core_utils.litellm_logging import Logging as LiteLLMLogging
from litellm.responses.litellm_completion_transformation.session_handler import ( from litellm.responses.litellm_completion_transformation.session_handler import (
ResponsesSessionHandler, ResponsesSessionHandler,
) )
from litellm.types.llms.base import CachedTokensDetails
from litellm.types.llms.openai import ( from litellm.types.llms.openai import (
AllMessageValues, AllMessageValues,
CachedTokensDetails,
ChatCompletionAssistantMessage, ChatCompletionAssistantMessage,
ChatCompletionImageObject, ChatCompletionImageObject,
ChatCompletionImageUrlObject, ChatCompletionImageUrlObject,

View file

@ -75,3 +75,9 @@ class HiddenParams(OpenAIObject):
data: Final = super().model_dump(**kwargs) data: Final = super().model_dump(**kwargs)
data["_response_ms"] = self._response_ms data["_response_ms"] = self._response_ms
return data return data
class CachedTokensDetails(BaseModel):
text_tokens: int | None = None
audio_tokens: int | None = None
image_tokens: int | None = None

View file

@ -82,7 +82,7 @@ from typing_extensions import (
override, override,
) )
from litellm.types.llms.base import BaseLiteLLMOpenAIResponseObject from litellm.types.llms.base import BaseLiteLLMOpenAIResponseObject, CachedTokensDetails
from litellm.types.responses.main import ( from litellm.types.responses.main import (
CustomToolCallOutputItem, CustomToolCallOutputItem,
GenericResponseOutputItem, GenericResponseOutputItem,
@ -1285,12 +1285,6 @@ class OutputTokensDetails(BaseLiteLLMOpenAIResponseObject):
model_config = {"extra": "allow"} model_config = {"extra": "allow"}
class CachedTokensDetails(BaseModel):
text_tokens: int | None = None
audio_tokens: int | None = None
image_tokens: int | None = None
class InputTokensDetails(BaseLiteLLMOpenAIResponseObject): class InputTokensDetails(BaseLiteLLMOpenAIResponseObject):
audio_tokens: int | None = None audio_tokens: int | None = None
cached_tokens: int = 0 cached_tokens: int = 0

View file

@ -48,6 +48,7 @@ from litellm._logging import verbose_logger
from litellm._uuid import uuid from litellm._uuid import uuid
from litellm.types.llms.base import ( from litellm.types.llms.base import (
BaseLiteLLMOpenAIResponseObject, BaseLiteLLMOpenAIResponseObject,
CachedTokensDetails,
LiteLLMPydanticObjectBase, LiteLLMPydanticObjectBase,
) )
from litellm.types.mcp import MCPServerCostInfo from litellm.types.mcp import MCPServerCostInfo
@ -60,7 +61,6 @@ from .llms.base import HiddenParams
from .llms.openai import ( from .llms.openai import (
AllMessageValues, AllMessageValues,
Batch, Batch,
CachedTokensDetails,
ChatCompletionAnnotation, ChatCompletionAnnotation,
ChatCompletionReasoningItem, ChatCompletionReasoningItem,
ChatCompletionRedactedThinkingBlock, ChatCompletionRedactedThinkingBlock,

View file

@ -19,6 +19,7 @@ from litellm.cost_calculator import (
) )
from litellm.litellm_core_utils.litellm_logging import Logging from litellm.litellm_core_utils.litellm_logging import Logging
from litellm.llms.base_llm.ocr.transformation import OCRPage, OCRResponse, OCRUsageInfo from litellm.llms.base_llm.ocr.transformation import OCRPage, OCRResponse, OCRUsageInfo
from litellm.types.llms.base import CachedTokensDetails
from litellm.types.llms.openai import OpenAIRealtimeStreamList from litellm.types.llms.openai import OpenAIRealtimeStreamList
from litellm.types.rerank import RerankResponse from litellm.types.rerank import RerankResponse
from litellm.types.utils import ( from litellm.types.utils import (
@ -4896,6 +4897,48 @@ def test_realtime_combine_sums_nested_cached_tokens_details():
assert combined.prompt_tokens_details.cached_tokens_details.image_tokens is None assert combined.prompt_tokens_details.cached_tokens_details.image_tokens is None
@pytest.mark.parametrize("details_first", [True, False])
def test_realtime_combine_keeps_cached_split_when_only_one_usage_has_details(details_first: bool):
with_details: Final = {
"type": "response.done",
"response": {
"usage": {
"input_tokens": 283,
"output_tokens": 0,
"total_tokens": 283,
"input_token_details": {
"text_tokens": 116,
"audio_tokens": 167,
"cached_tokens": 192,
"cached_tokens_details": {"text_tokens": 64, "audio_tokens": 128},
},
}
},
}
without_details: Final = {
"type": "response.done",
"response": {
"usage": {
"input_tokens": 150,
"output_tokens": 0,
"total_tokens": 150,
"input_token_details": {"text_tokens": 50, "audio_tokens": 100, "cached_tokens": 100},
}
},
}
results: OpenAIRealtimeStreamList = (
[with_details, without_details] if details_first else [without_details, with_details]
)
combined = RealtimeAPITokenUsageProcessor.collect_and_combine_usage_from_realtime_stream_results(
results=results,
)
assert combined.prompt_tokens_details is not None
assert combined.prompt_tokens_details.cached_tokens == 292
assert combined.prompt_tokens_details.cached_tokens_details == CachedTokensDetails(text_tokens=64, audio_tokens=128)
def test_usage_without_cached_tokens_details_omits_key(): def test_usage_without_cached_tokens_details_omits_key():
usage = Usage( usage = Usage(
prompt_tokens=10, prompt_tokens=10,

View file

@ -16781,7 +16781,6 @@ export interface paths {
* - permissions: Optional[dict] - [Not Implemented Yet] User-specific permissions, eg. turning off pii masking. * - permissions: Optional[dict] - [Not Implemented Yet] User-specific permissions, eg. turning off pii masking.
* - metadata: Optional[dict] - Metadata for user, store information for user. Example metadata = {"team": "core-infra", "app": "app2", "email": "ishaan@berri.ai" } * - metadata: Optional[dict] - Metadata for user, store information for user. Example metadata = {"team": "core-infra", "app": "app2", "email": "ishaan@berri.ai" }
* - max_parallel_requests: Optional[int] - Rate limit a user based on the number of parallel requests. Raises 429 error, if user's parallel requests > x. * - max_parallel_requests: Optional[int] - Rate limit a user based on the number of parallel requests. Raises 429 error, if user's parallel requests > x.
* - soft_budget: Optional[float] - Get alerts when user crosses given budget, doesn't block requests.
* - model_max_budget: Optional[dict] - Model-specific max budget for user. [Docs](https://docs.litellm.ai/docs/proxy/users#add-model-specific-budgets-to-keys) * - model_max_budget: Optional[dict] - Model-specific max budget for user. [Docs](https://docs.litellm.ai/docs/proxy/users#add-model-specific-budgets-to-keys)
* - budget_fallbacks: Optional[Dict[str, List[str]]] - Per-model fallback chain tried in order when that model's own `model_max_budget` is exceeded, e.g. {"gpt-4o": ["gpt-4o-mini"]}. * - budget_fallbacks: Optional[Dict[str, List[str]]] - Per-model fallback chain tried in order when that model's own `model_max_budget` is exceeded, e.g. {"gpt-4o": ["gpt-4o-mini"]}.
* - model_rpm_limit: Optional[float] - Model-specific rpm limit for user. [Docs](https://docs.litellm.ai/docs/proxy/users#add-model-specific-limits-to-keys) * - model_rpm_limit: Optional[float] - Model-specific rpm limit for user. [Docs](https://docs.litellm.ai/docs/proxy/users#add-model-specific-limits-to-keys)
@ -16887,7 +16886,6 @@ export interface paths {
* - permissions: Optional[dict] - [Not Implemented Yet] User-specific permissions, eg. turning off pii masking. * - permissions: Optional[dict] - [Not Implemented Yet] User-specific permissions, eg. turning off pii masking.
* - metadata: Optional[dict] - Metadata for user, store information for user. Example metadata = {"team": "core-infra", "app": "app2", "email": "ishaan@berri.ai" } * - metadata: Optional[dict] - Metadata for user, store information for user. Example metadata = {"team": "core-infra", "app": "app2", "email": "ishaan@berri.ai" }
* - max_parallel_requests: Optional[int] - Rate limit a user based on the number of parallel requests. Raises 429 error, if user's parallel requests > x. * - max_parallel_requests: Optional[int] - Rate limit a user based on the number of parallel requests. Raises 429 error, if user's parallel requests > x.
* - soft_budget: Optional[float] - Get alerts when user crosses given budget, doesn't block requests.
* - model_max_budget: Optional[dict] - Model-specific max budget for user. [Docs](https://docs.litellm.ai/docs/proxy/users#add-model-specific-budgets-to-keys) * - model_max_budget: Optional[dict] - Model-specific max budget for user. [Docs](https://docs.litellm.ai/docs/proxy/users#add-model-specific-budgets-to-keys)
* - budget_fallbacks: Optional[Dict[str, List[str]]] - Per-model fallback chain tried in order when that model's own `model_max_budget` is exceeded, e.g. {"gpt-4o": ["gpt-4o-mini"]}. * - budget_fallbacks: Optional[Dict[str, List[str]]] - Per-model fallback chain tried in order when that model's own `model_max_budget` is exceeded, e.g. {"gpt-4o": ["gpt-4o-mini"]}.
* - model_rpm_limit: Optional[float] - Model-specific rpm limit for user. [Docs](https://docs.litellm.ai/docs/proxy/users#add-model-specific-limits-to-keys) * - model_rpm_limit: Optional[float] - Model-specific rpm limit for user. [Docs](https://docs.litellm.ai/docs/proxy/users#add-model-specific-limits-to-keys)