Skip to content
55 changes: 55 additions & 0 deletions litellm/model_prices_and_context_window_backup.json
Original file line number Diff line number Diff line change
Expand Up @@ -25157,6 +25157,25 @@
"supports_vision": true,
"tool_use_system_prompt_tokens": 159
},
"openrouter/anthropic/claude-opus-4.6": {
"cache_creation_input_token_cost": 6.25e-06,
"cache_read_input_token_cost": 5e-07,
"input_cost_per_token": 5e-06,
"litellm_provider": "openrouter",
"max_input_tokens": 1000000,
"max_output_tokens": 128000,
"max_tokens": 128000,
"mode": "chat",
"output_cost_per_token": 2.5e-05,
"supports_assistant_prefill": true,
"supports_computer_use": true,
"supports_function_calling": true,
"supports_prompt_caching": true,
"supports_reasoning": true,
"supports_tool_choice": true,
"supports_vision": true,
"tool_use_system_prompt_tokens": 346
},
"openrouter/anthropic/claude-sonnet-4.5": {
"input_cost_per_image": 0.0048,
"cache_creation_input_token_cost": 3.75e-06,
Expand Down Expand Up @@ -26169,6 +26188,42 @@
"supports_prompt_caching": true,
"supports_computer_use": false
},
"openrouter/openrouter/auto": {
"input_cost_per_token": 0,
"output_cost_per_token": 0,
"litellm_provider": "openrouter",
"max_input_tokens": 2000000,
"max_tokens": 2000000,
"mode": "chat",
"supports_function_calling": true,
"supports_tool_choice": true,
"supports_reasoning": true,
"supports_response_schema": true,
"supports_vision": true,
"supports_audio_input": true,
"supports_video_input": true
},
"openrouter/openrouter/free": {
"input_cost_per_token": 0,
"output_cost_per_token": 0,
"litellm_provider": "openrouter",
"max_input_tokens": 200000,
"max_tokens": 200000,
"mode": "chat",
"supports_function_calling": true,
"supports_tool_choice": true,
"supports_reasoning": true,
"supports_response_schema": true,
"supports_vision": true
},
"openrouter/openrouter/bodybuilder": {
"input_cost_per_token": 0,
"output_cost_per_token": 0,
"litellm_provider": "openrouter",
"max_input_tokens": 128000,
"max_tokens": 128000,
"mode": "chat"
},
"ovhcloud/DeepSeek-R1-Distill-Llama-70B": {
"input_cost_per_token": 6.7e-07,
"litellm_provider": "ovhcloud",
Expand Down
84 changes: 82 additions & 2 deletions litellm/proxy/auth/handle_jwt.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@

import fnmatch
import os
import re
from typing import Any, List, Literal, Optional, Set, Tuple, cast

from cryptography import x509
Expand Down Expand Up @@ -235,7 +236,17 @@ def get_team_id(self, token: dict, default_value: Optional[str]) -> Optional[str
return self.litellm_jwtauth.team_id_default
else:
return default_value
# At this point, team_id is not the sentinel, so it should be a string
# AAD and other IdPs often send roles/groups as a list of strings.
# team_id_jwt_field is singular, so take the first element when a list
# is returned. This avoids "unhashable type: 'list'" errors downstream.
if isinstance(team_id, list):
if not team_id:
return default_value
verbose_proxy_logger.debug(
f"JWT Auth: team_id_jwt_field '{self.litellm_jwtauth.team_id_jwt_field}' "
f"returned a list {team_id}; using first element '{team_id[0]}' automatically."
)
team_id = team_id[0]
return team_id # type: ignore[return-value]
elif self.litellm_jwtauth.team_id_default is not None:
team_id = self.litellm_jwtauth.team_id_default
Expand Down Expand Up @@ -453,6 +464,52 @@ def get_scopes(self, token: dict) -> List[str]:
scopes = []
return scopes

async def _resolve_jwks_url(self, url: str) -> str:
"""
If url points to an OIDC discovery document (*.well-known/openid-configuration),
fetch it and return the jwks_uri contained within. Otherwise return url unchanged.
This lets JWT_PUBLIC_KEY_URL be set to a well-known discovery endpoint instead of
requiring operators to manually find the JWKS URL.
"""
if ".well-known/openid-configuration" not in url:
return url

cache_key = f"litellm_oidc_discovery_{url}"
cached_jwks_uri = await self.user_api_key_cache.async_get_cache(cache_key)
if cached_jwks_uri is not None:
return cached_jwks_uri

verbose_proxy_logger.debug(
f"JWT Auth: Fetching OIDC discovery document from {url}"
)
response = await self.http_handler.get(url)
if response.status_code != 200:
raise Exception(
f"JWT Auth: OIDC discovery endpoint {url} returned status {response.status_code}: {response.text}"
)
try:
discovery = response.json()
except Exception as e:
raise Exception(
f"JWT Auth: Failed to parse OIDC discovery document at {url}: {e}"
)

jwks_uri = discovery.get("jwks_uri")
if not jwks_uri:
raise Exception(
f"JWT Auth: OIDC discovery document at {url} does not contain a 'jwks_uri' field."
)

verbose_proxy_logger.debug(
f"JWT Auth: Resolved OIDC discovery {url} -> jwks_uri={jwks_uri}"
)
await self.user_api_key_cache.async_set_cache(
key=cache_key,
value=jwks_uri,
ttl=self.litellm_jwtauth.public_key_ttl,
)
return jwks_uri

async def get_public_key(self, kid: Optional[str]) -> dict:
keys_url = os.getenv("JWT_PUBLIC_KEY_URL")

Expand All @@ -462,6 +519,7 @@ async def get_public_key(self, kid: Optional[str]) -> dict:
keys_url_list = [url.strip() for url in keys_url.split(",")]

for key_url in keys_url_list:
key_url = await self._resolve_jwks_url(key_url)
cache_key = f"litellm_jwt_auth_keys_{key_url}"

cached_keys = await self.user_api_key_cache.async_get_cache(cache_key)
Expand Down Expand Up @@ -913,8 +971,30 @@ async def find_and_validate_specific_team_id(
if jwt_handler.is_required_team_id() is True:
team_id_field = jwt_handler.litellm_jwtauth.team_id_jwt_field
team_alias_field = jwt_handler.litellm_jwtauth.team_alias_jwt_field
hint = ""
if team_id_field:
# "roles.0" — dot-notation numeric indexing is not supported
if "." in team_id_field:
parts = team_id_field.rsplit(".", 1)
if parts[-1].isdigit():
base_field = parts[0]
hint = (
f" Hint: dot-notation array indexing (e.g. '{team_id_field}') is not "
f"supported. Use '{base_field}' instead — LiteLLM automatically "
f"uses the first element when the field value is a list."
)
# "roles[0]" — bracket-notation indexing is also not supported in get_nested_value
elif "[" in team_id_field and team_id_field.endswith("]"):
m = re.match(r"^(\w+)\[(\d+)\]$", team_id_field)
if m:
base_field = m.group(1)
hint = (
f" Hint: array indexing (e.g. '{team_id_field}') is not supported "
f"in team_id_jwt_field. Use '{base_field}' instead — LiteLLM "
f"automatically uses the first element when the field value is a list."
)
raise Exception(
f"No team found in token. Checked team_id field '{team_id_field}' and team_alias field '{team_alias_field}'"
f"No team found in token. Checked team_id field '{team_id_field}' and team_alias field '{team_alias_field}'.{hint}"
)

return individual_team_id, team_object
Expand Down
Loading
Loading