Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
27 changes: 1 addition & 26 deletions docs/my-website/docs/provider_registration/add_model_pricing.md
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,6 @@ Here's the full specification with all available fields:
```json
{
"sample_spec": {
"aliases": ["optional list of alternate names for this model, e.g. dated versions like sample_spec-20250101"],
"code_interpreter_cost_per_session": 0.0,
"computer_use_input_cost_per_1k_tokens": 0.0,
"computer_use_output_cost_per_1k_tokens": 0.0,
Expand Down Expand Up @@ -122,28 +121,4 @@ Here's the full specification with all available fields:
}
```

### Using Aliases

Many providers release the same model under multiple names — for example, a `latest` tag and a dated version like `claude-sonnet-4-5-20250929`. Instead of duplicating the entire entry, you can use the `aliases` field:

```json
{
"claude-sonnet-4-5": {
"aliases": ["claude-sonnet-4-5-20250929"],
"input_cost_per_token": 3e-06,
"output_cost_per_token": 1.5e-05,
"litellm_provider": "anthropic",
"max_input_tokens": 200000,
"max_output_tokens": 64000,
"mode": "chat",
"supports_function_calling": true,
"supports_tool_choice": true
}
}
```

At load time, each alias is expanded into a top-level entry sharing the same data as the canonical entry. The example above makes both `claude-sonnet-4-5` and `claude-sonnet-4-5-20250929` resolve with the same pricing and capabilities.

:::info
This is different from [`model_alias_map`](../completion/model_alias.md), which is a runtime SDK/proxy feature for mapping user-facing model names to LiteLLM model identifiers. The `aliases` field here is for the model cost JSON only — it avoids duplicate entries for models that share identical pricing and capabilities.
:::
That's it! Your PR will be reviewed and merged.
4 changes: 0 additions & 4 deletions litellm/caching/dual_cache.py
Original file line number Diff line number Diff line change
Expand Up @@ -346,8 +346,6 @@ async def async_set_cache(self, key, value, local_only: bool = False, **kwargs):
)
try:
if self.in_memory_cache is not None:
if "ttl" not in kwargs and self.default_in_memory_ttl is not None:
kwargs["ttl"] = self.default_in_memory_ttl
await self.in_memory_cache.async_set_cache(key, value, **kwargs)

if self.redis_cache is not None and local_only is False:
Comment on lines 346 to 351

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

default_in_memory_ttl no longer applied in async_set_cache and async_set_cache_pipeline

The revert removes these lines from both async_set_cache (line 346) and async_set_cache_pipeline (line 367):

if "ttl" not in kwargs and self.default_in_memory_ttl is not None:
    kwargs["ttl"] = self.default_in_memory_ttl

Any caller that relies on DualCache.default_in_memory_ttl being automatically applied when no explicit TTL is passed will now store entries in the in-memory cache without a TTL, causing them to never expire. This is a silent behavioral regression — entries that should have been evicted after the default TTL will now persist indefinitely in memory.

Expand All @@ -369,8 +367,6 @@ async def async_set_cache_pipeline(
)
try:
if self.in_memory_cache is not None:
if "ttl" not in kwargs and self.default_in_memory_ttl is not None:
kwargs["ttl"] = self.default_in_memory_ttl
await self.in_memory_cache.async_set_cache_pipeline(
cache_list=cache_list, **kwargs
)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -390,7 +390,6 @@ def _convert_response_output_to_choices(
ResponseOutputMessage,
ResponseReasoningItem,
)
from openai.types.responses.response_output_item import ResponseApplyPatchToolCall

from litellm.types.utils import Choices, Message

Expand Down Expand Up @@ -449,18 +448,6 @@ def _convert_response_output_to_choices(
accumulated_tool_calls.append(tool_call_dict)
tool_call_index += 1

elif isinstance(item, ResponseApplyPatchToolCall):
from litellm.responses.litellm_completion_transformation.transformation import (
LiteLLMCompletionResponsesConfig,
)

tool_call_dict = LiteLLMCompletionResponsesConfig.convert_apply_patch_tool_call_to_chat_completion_tool_call(
tool_call_item=item,
index=tool_call_index,
)
accumulated_tool_calls.append(tool_call_dict)
tool_call_index += 1

elif isinstance(item, dict) and handle_raw_dict_callback is not None:
# Handle raw dict responses (e.g., from GPT-5 Codex)
choice, index = handle_raw_dict_callback(item=item, index=index)
Expand Down Expand Up @@ -1108,21 +1095,14 @@ def translate_responses_chunk_to_openai_stream( # noqa: PLR0915

finish_reason = "tool_calls" if has_function_calls else "stop"

usage = None
if response_data.get("usage"):
from litellm.responses.utils import ResponseAPILoggingUtils
usage = ResponseAPILoggingUtils._transform_response_api_usage_to_chat_usage(
response_data.get("usage")
)
return ModelResponseStream(
choices=[
StreamingChoices(
index=0,
delta=Delta(content=""),
finish_reason=finish_reason,
)
],
usage=usage
]
)
else:
pass
Expand Down
10 changes: 6 additions & 4 deletions litellm/litellm_core_utils/duration_parser.py
Original file line number Diff line number Diff line change
Expand Up @@ -64,10 +64,12 @@ def duration_in_seconds(duration: str) -> int:
now = time.time()
current_time = datetime.fromtimestamp(now)

# Calculate target month and year, handling overflow past December
total_months = current_time.month - 1 + value # 0-indexed months
target_year = current_time.year + total_months // 12
target_month = total_months % 12 + 1 # back to 1-indexed
if current_time.month == 12:
target_year = current_time.year + 1
target_month = 1
else:
target_year = current_time.year
target_month = current_time.month + value
Comment on lines +67 to +72

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Month overflow not handled for value > 1

The reverted code only handles overflow when current_time.month == 12, but does nothing when current_time.month + value > 12 for other months. For example, if today is October (month=10) and value=3, then target_month = 13, which is an invalid month and will raise a ValueError when the datetime constructor is called on line 81.

The code removed by this revert handled this correctly using modular arithmetic:

total_months = current_time.month - 1 + value  # 0-indexed months
target_year = current_time.year + total_months // 12
target_month = total_months % 12 + 1  # back to 1-indexed

With the current reverted code, any duration string like "3mo" or "2mo" used between October–November will crash at runtime.


# Determine the day to set for next month
target_day = current_time.day
Expand Down
58 changes: 5 additions & 53 deletions litellm/litellm_core_utils/get_model_cost_map.py
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@
import json
import os
from importlib.resources import files
from typing import Dict, List, Optional
from typing import Optional

import httpx

Expand Down Expand Up @@ -183,54 +183,6 @@ def get_model_cost_map_source_info() -> dict:
}


def _expand_model_aliases(model_cost: dict) -> dict:
"""
Expand ``aliases`` lists in model cost entries into top-level entries.

Each alias gets a reference to the **same** dict object as the canonical
entry (zero memory overhead). The ``aliases`` key is removed from the
entry so downstream code never sees it.

If an alias collides with an existing canonical entry the alias is
silently skipped and a warning is logged.
"""
aliases_to_add: Dict[str, dict] = {}
keys_with_aliases: List[str] = []

for model_name, model_info in model_cost.items():
aliases: Optional[list] = model_info.get("aliases")
if aliases is None:
continue
keys_with_aliases.append(model_name)
if not aliases:
continue
for alias in aliases:
if alias in model_cost:
verbose_logger.warning(
"LiteLLM model alias conflict: alias '%s' (from '%s') "
"already exists as a canonical entry — skipping.",
alias,
model_name,
)
continue
if alias in aliases_to_add:
verbose_logger.warning(
"LiteLLM model alias conflict: alias '%s' (from '%s') "
"was already claimed by another entry — skipping.",
alias,
model_name,
)
continue
aliases_to_add[alias] = model_info # same dict reference

# Remove the ``aliases`` key from entries so it doesn't pollute model info
for key in keys_with_aliases:
model_cost[key].pop("aliases", None)

model_cost.update(aliases_to_add)
return model_cost


def get_model_cost_map(url: str) -> dict:
"""
Public entry point — returns the model cost map dict.
Expand All @@ -250,7 +202,7 @@ def get_model_cost_map(url: str) -> dict:
_cost_map_source_info.url = None
_cost_map_source_info.is_env_forced = True
_cost_map_source_info.fallback_reason = None
return _expand_model_aliases(GetModelCostMap.load_local_model_cost_map())
return GetModelCostMap.load_local_model_cost_map()

_cost_map_source_info.url = url
_cost_map_source_info.is_env_forced = False
Expand All @@ -266,7 +218,7 @@ def get_model_cost_map(url: str) -> dict:
)
_cost_map_source_info.source = "local"
_cost_map_source_info.fallback_reason = f"Remote fetch failed: {str(e)}"
return _expand_model_aliases(GetModelCostMap.load_local_model_cost_map())
return GetModelCostMap.load_local_model_cost_map()

# Validate using cached count (cheap int comparison, no file I/O)
if not GetModelCostMap.validate_model_cost_map(
Expand All @@ -280,8 +232,8 @@ def get_model_cost_map(url: str) -> dict:
)
_cost_map_source_info.source = "local"
_cost_map_source_info.fallback_reason = "Remote data failed integrity validation"
return _expand_model_aliases(GetModelCostMap.load_local_model_cost_map())
return GetModelCostMap.load_local_model_cost_map()

_cost_map_source_info.source = "remote"
_cost_map_source_info.fallback_reason = None
return _expand_model_aliases(content)
return content
70 changes: 0 additions & 70 deletions litellm/litellm_core_utils/redact_messages.py
Original file line number Diff line number Diff line change
Expand Up @@ -73,53 +73,6 @@ def _redact_responses_api_output(output_items):
summary_item.text = "redacted-by-litellm"


def _redact_standard_logging_object(model_call_details: dict):
"""Redact messages and response inside standard_logging_object if present."""
standard_logging_object = model_call_details.get("standard_logging_object")
if standard_logging_object is None:
return

redacted_str = "redacted-by-litellm"

if standard_logging_object.get("messages") is not None:
standard_logging_object["messages"] = [
{"role": "user", "content": redacted_str}
]

response = standard_logging_object.get("response")
if response is not None:
if isinstance(response, dict) and "output" in response:
# ResponsesAPIResponse format - redact content in output items
if isinstance(response.get("output"), list):
for output_item in response["output"]:
if isinstance(output_item, dict) and "content" in output_item:
if isinstance(output_item["content"], list):
for content_item in output_item["content"]:
if (
isinstance(content_item, dict)
and "text" in content_item
):
content_item["text"] = redacted_str
elif isinstance(response, dict) and "choices" in response:
# ModelResponse dict format - redact content in choices
if isinstance(response.get("choices"), list):
for choice in response["choices"]:
if isinstance(choice, dict):
if "message" in choice and isinstance(choice["message"], dict):
choice["message"]["content"] = redacted_str
if "audio" in choice["message"]:
choice["message"]["audio"] = None
elif "delta" in choice and isinstance(choice["delta"], dict):
choice["delta"]["content"] = redacted_str
if "audio" in choice["delta"]:
choice["delta"]["audio"] = None
elif isinstance(response, str):
standard_logging_object["response"] = redacted_str
else:
# For other formats (empty dict, None, etc.), use simple text format
standard_logging_object["response"] = {"text": redacted_str}


def perform_redaction(model_call_details: dict, result):
"""
Performs the actual redaction on the logging object and result.
Expand Down Expand Up @@ -161,29 +114,6 @@ def perform_redaction(model_call_details: dict, result):
if hasattr(_result, "choices") and _result.choices is not None:
for choice in _result.choices:
_redact_choice_content(choice)
elif isinstance(_result, dict) and "choices" in _result:
# Handle dict representation of ModelResponse (e.g., from model_dump())
if _result.get("choices") is not None:
for choice in _result["choices"]:
if isinstance(choice, dict):
if "message" in choice and isinstance(choice["message"], dict):
choice["message"]["content"] = "redacted-by-litellm"
if "reasoning_content" in choice["message"]:
choice["message"]["reasoning_content"] = "redacted-by-litellm"
if "thinking_blocks" in choice["message"]:
choice["message"]["thinking_blocks"] = None
if "audio" in choice["message"]:
choice["message"]["audio"] = None
elif "delta" in choice and isinstance(choice["delta"], dict):
choice["delta"]["content"] = "redacted-by-litellm"
if "reasoning_content" in choice["delta"]:
choice["delta"]["reasoning_content"] = "redacted-by-litellm"
if "thinking_blocks" in choice["delta"]:
choice["delta"]["thinking_blocks"] = None
if "audio" in choice["delta"]:
choice["delta"]["audio"] = None
else:
_redact_choice_content(choice)
elif isinstance(_result, litellm.ResponsesAPIResponse):
if hasattr(_result, "output"):
_redact_responses_api_output(_result.output)
Expand Down
24 changes: 5 additions & 19 deletions litellm/llms/azure/chat/gpt_5_transformation.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,10 +4,7 @@

import litellm
from litellm.exceptions import UnsupportedParamsError
from litellm.llms.openai.chat.gpt_5_transformation import (
OpenAIGPT5Config,
_get_effort_level,
)
from litellm.llms.openai.chat.gpt_5_transformation import OpenAIGPT5Config
from litellm.types.llms.openai import AllMessageValues

from .gpt_transformation import AzureOpenAIConfig
Expand Down Expand Up @@ -84,21 +81,20 @@ def map_openai_params(
non_default_params.get("reasoning_effort")
or optional_params.get("reasoning_effort")
)
effective_effort = _get_effort_level(reasoning_effort_value)

# gpt-5.1/5.2/5.4 support reasoning_effort='none', but other gpt-5 models don't
# See: https://learn.microsoft.com/en-us/azure/ai-foundry/openai/how-to/reasoning
supports_none = self._supports_reasoning_effort_level(model, "none")

if effective_effort == "none" and not supports_none:
if reasoning_effort_value == "none" and not supports_none:
if litellm.drop_params is True or (
drop_params is not None and drop_params is True
):
non_default_params = non_default_params.copy()
optional_params = optional_params.copy()
if _get_effort_level(non_default_params.get("reasoning_effort")) == "none":
if non_default_params.get("reasoning_effort") == "none":
non_default_params.pop("reasoning_effort")
if _get_effort_level(optional_params.get("reasoning_effort")) == "none":
if optional_params.get("reasoning_effort") == "none":
optional_params.pop("reasoning_effort")
else:
raise UnsupportedParamsError(
Expand All @@ -121,19 +117,9 @@ def map_openai_params(
)

# Only drop reasoning_effort='none' for models that don't support it
result_effort = _get_effort_level(result.get("reasoning_effort"))
if result_effort == "none" and not supports_none:
if result.get("reasoning_effort") == "none" and not supports_none:
result.pop("reasoning_effort")

# Azure Chat Completions: gpt-5.4+ does not support tools + reasoning together.
# Drop reasoning_effort when both are present (OpenAI routes to Responses API; Azure does not).
if self.is_model_gpt_5_4_plus_model(model):
has_tools = bool(
non_default_params.get("tools") or optional_params.get("tools")
)
if has_tools and result_effort not in (None, "none"):
result.pop("reasoning_effort", None)

return result

def transform_request(
Expand Down
Loading
Loading