mirror of
https://github.com/BerriAI/litellm.git
synced 2026-11-12 08:42:18 +00:00
feat(proxy): give Codex accurate model entries for every model in /v1/models (#49825)
* feat(proxy): give Codex accurate model entries for every model in /v1/models * fix(router): read a deployment's upstream model without a not-required key access * fix(proxy): keep Codex tiers for unpriced deployments and match stock entries by declared provider A deployment with no token price no longer votes a Codex service tier off, an undescribed model keeps Codex's default reasoning summary parameter, a Bedrock deployment naming its provider in custom_llm_provider gets Codex's stock entry, and Bedrock provider namespaces follow the AWS model id format. * fix(proxy): import cost tracking and provider helpers lazily in the Codex catalog lite codex imports the catalog before the SDK is initialized, so importing cost tracking, the Bedrock helpers or provider resolution at module level hit a circular import and broke every lite command. * fix(proxy): drop Codex's fast speed tier wherever its priority tier is dropped Codex shows /fast for a leftover fast speed tier even when service_tiers no longer offers priority, so an alias, an empty configured tier list or a configured list without priority showed a /fast toggle that sends nothing. One helper now sets service tiers, speed tiers and the default together. * fix(proxy): give an older Codex a translated entry for models past its version Codex's own entries name the oldest release that can read them in minimal_client_version. A release older than that can fail to decode the entry, such as a reasoning effort it has no name for, which fails the whole catalog, so it now gets the translated entry, whose levels are trimmed to that release
This commit is contained in:
parent
a1b87bcca8
commit
9a473fbbb0
17 changed files with 1324 additions and 200 deletions
|
|
@ -1538,6 +1538,19 @@ def get_bedrock_base_model(model: str) -> str:
|
|||
return model
|
||||
|
||||
|
||||
def bedrock_model_name_without_provider(model: str) -> str:
|
||||
"""The model id without Bedrock's routing and region prefixes and without its provider namespace.
|
||||
|
||||
A Bedrock foundation model id is ``<provider>.<model>`` with a one-word provider (``anthropic.``,
|
||||
``deepseek.``, ``moonshotai.``), so a base model whose first dot-separated part is one word drops it:
|
||||
``bedrock/us.openai.gpt-6.1-sol`` and ``us-east-1/openai.gpt-6.1-sol`` are ``gpt-6.1-sol``,
|
||||
``us.deepseek.r1-v1:0`` is ``r1-v1:0``. Any other base model, such as ``gpt-5.5`` or an imported model's
|
||||
ARN id, is returned as is."""
|
||||
base: Final = get_bedrock_base_model(model)
|
||||
provider, separator, name = base.partition(".")
|
||||
return name if separator and provider.isalnum() else base
|
||||
|
||||
|
||||
def bedrock_converse_accepts_redacted_thinking(model: str, base_model: str | None) -> bool:
|
||||
"""
|
||||
Whether ``reasoningContent.redactedContent`` may be replayed to this Converse target.
|
||||
|
|
|
|||
File diff suppressed because one or more lines are too long
|
|
@ -3,37 +3,50 @@
|
|||
Codex CLI discovers a provider's models with ``GET <model_catalog_url>?client_version=<its version>``
|
||||
(the provider's ``model_catalog_url`` pointed at ``/v1/models``, with its ``api_key_model_discovery``
|
||||
feature on) and decodes the body as its own ``ModelsResponse``, ``{"models": [ModelInfo, ...]}``,
|
||||
never the OpenAI list shape. This module builds that body from the rows ``/v1/models`` already
|
||||
lists: a model Codex knows keeps Codex's own entry for it, any other model gets the fallback entry
|
||||
Codex itself uses for an unknown slug, and ``model_info.service_tiers`` becomes the entry's
|
||||
never the OpenAI list shape. Codex then ignores its own bundled catalog, so this body is all it knows
|
||||
about the provider's models. It is built from the rows ``/v1/models`` already lists: a model Codex
|
||||
knows, by its id or by the upstream model its deployments call once the provider's own prefixes are
|
||||
stripped, keeps Codex's own entry for it, minus any service tier some deployment behind it prices tokens
|
||||
but not that tier for; any other model gets an entry translated from the capabilities its deployments' model info
|
||||
declares (see ``codex_model_translation``). ``model_info.service_tiers`` becomes the entry's
|
||||
``service_tiers``, which Codex turns into slash commands (``/ultrafast``) that send ``service_tier``
|
||||
upstream. A ``model_info.max_input_tokens`` the operator sets replaces a known model's
|
||||
``context_window``, up to the most Codex lets its own ``model_context_window`` raise that model to.
|
||||
A fallback entry offers the model group's ``supported_reasoning_efforts`` as its reasoning levels,
|
||||
A translated entry offers the model group's ``supported_reasoning_efforts`` as its reasoning levels,
|
||||
trimmed to the ones the requesting Codex release can parse.
|
||||
|
||||
The stock entries are Codex 0.159.3's bundled catalog, vendored unchanged from
|
||||
https://github.com/openai/codex/blob/rust-v0.159.3/codex-rs/models-manager/models.json (Apache-2.0)
|
||||
as ``codex_bundled_models_0.159.3.json``, and ``codex_base_instructions.md`` is
|
||||
The stock entries are Codex 0.166.0's bundled catalog, vendored unchanged from
|
||||
https://github.com/openai/codex/blob/rust-v0.166.0/codex-rs/models-manager/models.json (Apache-2.0)
|
||||
as ``codex_bundled_models_0.166.0.json``, and ``codex_base_instructions.md`` is
|
||||
``codex-rs/models-manager/prompt.md`` at the same tag. Refresh both by downloading those two paths at
|
||||
a newer tag.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from collections.abc import Mapping, Sequence
|
||||
from collections.abc import Callable, Mapping, Sequence
|
||||
from dataclasses import dataclass
|
||||
from functools import cache
|
||||
from itertools import accumulate, pairwise
|
||||
from pathlib import Path
|
||||
from types import MappingProxyType, NoneType
|
||||
from typing import TYPE_CHECKING, Annotated, Final, Literal, TypeAlias
|
||||
from types import MappingProxyType
|
||||
from typing import TYPE_CHECKING, Final, Literal, TypeAlias
|
||||
|
||||
from packaging.version import Version
|
||||
from pydantic import ConfigDict, Field, StringConstraints, TypeAdapter, ValidationError
|
||||
from pydantic import ConfigDict, TypeAdapter, ValidationError
|
||||
|
||||
from litellm._logging import verbose_proxy_logger
|
||||
from litellm.litellm_core_utils.reasoning_effort_strength import nearest_declared_reasoning_effort
|
||||
from litellm.proxy.common_utils.codex_model_translation import (
|
||||
TOKEN_PRICE_KEY,
|
||||
CodexDeploymentCapabilities,
|
||||
CodexFallbackModel,
|
||||
CodexServiceTier,
|
||||
TierId,
|
||||
codex_fallback_entry,
|
||||
combined_capabilities,
|
||||
no_priced_deployment_lacks_tier,
|
||||
tier_price_keys,
|
||||
)
|
||||
from litellm.types.llms.base_model import LiteLLMBaseModel
|
||||
|
||||
if TYPE_CHECKING:
|
||||
|
|
@ -42,89 +55,24 @@ if TYPE_CHECKING:
|
|||
from litellm.types.proxy.model_listing import ModelInfoResponse
|
||||
|
||||
CODEX_BASE_INSTRUCTIONS_PATH: Final = Path(__file__).with_name("codex_base_instructions.md")
|
||||
CODEX_BUNDLED_MODELS_PATH: Final = Path(__file__).with_name("codex_bundled_models_0.159.3.json")
|
||||
CODEX_BUNDLED_MODELS_PATH: Final = Path(__file__).with_name("codex_bundled_models_0.166.0.json")
|
||||
CODEX_CATALOG_BYTE_LIMIT: Final = 1024 * 1024
|
||||
CODEX_CHAT_MODES: Final = frozenset({"chat", "responses"})
|
||||
|
||||
_TierId = Annotated[str, StringConstraints(strip_whitespace=True, min_length=1)]
|
||||
_BODY_PREFIX: Final = '{"models":['
|
||||
_BODY_SUFFIX: Final = "]}"
|
||||
_FIRST_CODEX_WITH_CUSTOM_EFFORTS: Final = Version("0.138.0")
|
||||
_EFFORTS_EVERY_CODEX_PARSES: Final = frozenset({"none", "minimal", "low", "medium", "high", "xhigh"})
|
||||
_PREFERRED_DEFAULT_EFFORT: Final = "medium"
|
||||
_CODEX_UNSET_EFFORT: Final = "none"
|
||||
|
||||
|
||||
class CodexServiceTier(LiteLLMBaseModel):
|
||||
"""A `ModelServiceTier` as Codex reads it: the slash command is `name` lowercased, and toggling
|
||||
it sends `id` as the request's `service_tier`."""
|
||||
|
||||
model_config = ConfigDict(frozen=True)
|
||||
|
||||
id: _TierId
|
||||
name: str
|
||||
description: str
|
||||
_NO_UPDATE: Final[Mapping[str, object]] = MappingProxyType({})
|
||||
|
||||
|
||||
class _ConfiguredTier(LiteLLMBaseModel):
|
||||
model_config = ConfigDict(frozen=True, extra="forbid")
|
||||
|
||||
id: _TierId
|
||||
id: TierId
|
||||
name: str | None = None
|
||||
description: str | None = None
|
||||
|
||||
|
||||
_CONFIGURED_TIERS: Final = TypeAdapter(tuple[_TierId | _ConfiguredTier, ...])
|
||||
|
||||
|
||||
class CodexFallbackReasoningLevel(LiteLLMBaseModel):
|
||||
"""A `ReasoningEffortPreset` as Codex reads it: picking it sends `effort` as the request's reasoning effort."""
|
||||
|
||||
model_config = ConfigDict(frozen=True)
|
||||
|
||||
effort: str
|
||||
description: str
|
||||
|
||||
|
||||
class CodexTruncationPolicy(LiteLLMBaseModel):
|
||||
mode: Literal["bytes"] = "bytes"
|
||||
limit: int = 10_000
|
||||
|
||||
|
||||
class CodexFallbackModel(LiteLLMBaseModel):
|
||||
"""One `ModelInfo` entry of a Codex model catalog for a model Codex does not know.
|
||||
|
||||
Every field that some Codex release since `model_catalog_json` appeared
|
||||
(0.105.0) deserializes without a default is spelled out here, so one catalog
|
||||
parses on all of them; the values match the fallback metadata Codex uses for
|
||||
a model slug it does not know, so picking such a proxy model behaves the
|
||||
same as `codex -m` did.
|
||||
"""
|
||||
|
||||
model_config = ConfigDict(frozen=True)
|
||||
|
||||
slug: str
|
||||
display_name: str
|
||||
description: NoneType = None
|
||||
default_reasoning_level: str | None = Field(default=None, exclude_if=lambda level: level is None)
|
||||
supported_reasoning_levels: tuple[CodexFallbackReasoningLevel, ...] = ()
|
||||
shell_type: Literal["unified_exec"] = "unified_exec"
|
||||
visibility: Literal["list"] = "list"
|
||||
supported_in_api: Literal[True] = True
|
||||
priority: int
|
||||
service_tiers: tuple[CodexServiceTier, ...] = ()
|
||||
availability_nux: NoneType = None
|
||||
upgrade: NoneType = None
|
||||
support_verbosity: Literal[False] = False
|
||||
supports_reasoning_summaries: Literal[False] = False
|
||||
supports_parallel_tool_calls: Literal[False] = False
|
||||
default_verbosity: NoneType = None
|
||||
apply_patch_tool_type: NoneType = None
|
||||
truncation_policy: CodexTruncationPolicy = CodexTruncationPolicy()
|
||||
experimental_supported_tools: tuple[()] = ()
|
||||
context_window: int | None
|
||||
base_instructions: str
|
||||
supports_search_tool: bool = False
|
||||
_CONFIGURED_TIERS: Final = TypeAdapter(tuple[TierId | _ConfiguredTier, ...])
|
||||
|
||||
|
||||
class CodexStockUpgrade(LiteLLMBaseModel):
|
||||
|
|
@ -156,11 +104,13 @@ class CodexStockModel(LiteLLMBaseModel):
|
|||
supported_in_api: bool = True
|
||||
upgrade: CodexStockUpgrade | None = None
|
||||
service_tiers: tuple[CodexServiceTier, ...] = ()
|
||||
additional_speed_tiers: tuple[str, ...] = ()
|
||||
default_service_tier: str | None = None
|
||||
supports_search_tool: bool = False
|
||||
supported_reasoning_levels: tuple[CodexReasoningLevel, ...] = ()
|
||||
context_window: int | None = None
|
||||
max_context_window: int | None = None
|
||||
minimal_client_version: str | tuple[int, ...] | None = None
|
||||
|
||||
|
||||
class CodexStockCatalog(LiteLLMBaseModel):
|
||||
|
|
@ -174,22 +124,26 @@ CodexAliasDefaults: TypeAlias = Literal["codex", "deployment"]
|
|||
@dataclass(frozen=True, slots=True)
|
||||
class CodexCatalogRow:
|
||||
"""What the catalog needs to know about one listed model: its public id, the listing's mode and
|
||||
input limit, the upstream model behind it, the reasoning effort its deployments pin, and the
|
||||
input limit, the upstream model behind it and the provider its deployment declares, the reasoning
|
||||
effort its deployments pin, and the
|
||||
`model_info` values the operator set; `service_tiers` holds one raw `model_info.service_tiers`
|
||||
value per deployment behind the id, None where a deployment sets none, and
|
||||
`configured_max_input_tokens` one `model_info.max_input_tokens` per deployment the same way.
|
||||
`supported_reasoning_efforts` is the model group's, None when unknown."""
|
||||
`deployment_capabilities` holds what each deployment's model info, merged with the cost map,
|
||||
declares. `supported_reasoning_efforts` is the model group's, None when unknown."""
|
||||
|
||||
id: str
|
||||
mode: str | None = None
|
||||
max_input_tokens: int | None = None
|
||||
upstream_model: str | None = None
|
||||
upstream_provider: str | None = None
|
||||
reasoning_effort: str | None = None
|
||||
display_name: str | None = None
|
||||
service_tiers: tuple[object, ...] = ()
|
||||
supported_reasoning_efforts: tuple[str, ...] | None = None
|
||||
search_tool_support: tuple[bool | None, ...] = ()
|
||||
configured_max_input_tokens: tuple[int | None, ...] = ()
|
||||
deployment_capabilities: tuple[CodexDeploymentCapabilities, ...] = ()
|
||||
|
||||
|
||||
@cache
|
||||
|
|
@ -292,44 +246,87 @@ def _codex_can_drive(model_id: str, mode: str | None) -> bool:
|
|||
return "*" not in model_id and (mode is None or mode in CODEX_CHAT_MODES)
|
||||
|
||||
|
||||
def _parses_custom_efforts(client_version: str | None) -> bool:
|
||||
@cache
|
||||
def _provider_model_names() -> Mapping[str, Callable[[str], str]]:
|
||||
"""Imported on first use for the same reason as `tier_price_keys`."""
|
||||
from litellm.llms.bedrock.common_utils import bedrock_model_name_without_provider
|
||||
|
||||
return MappingProxyType(
|
||||
{"bedrock": bedrock_model_name_without_provider, "bedrock_mantle": bedrock_model_name_without_provider}
|
||||
)
|
||||
|
||||
|
||||
def _provider_model_name(upstream_model: str, upstream_provider: str | None) -> str:
|
||||
"""The upstream model without the prefixes its provider adds, the provider being the one the deployment
|
||||
declares in `custom_llm_provider`, else the model's `provider/` prefix."""
|
||||
from litellm.litellm_core_utils.get_llm_provider_logic import declared_provider
|
||||
|
||||
provider: Final = declared_provider(upstream_model, upstream_provider)
|
||||
if provider is None:
|
||||
return upstream_model
|
||||
model: Final = upstream_model.removeprefix(f"{provider}/")
|
||||
name_for: Final = _provider_model_names().get(provider)
|
||||
return model if name_for is None else name_for(model)
|
||||
|
||||
|
||||
def _stock_for(row: CodexCatalogRow, stock: Mapping[str, CodexStockModel]) -> CodexStockModel | None:
|
||||
"""Codex's entry for the row's id, else for its upstream model as written, else for that model's name
|
||||
once its provider's own prefixes are stripped (`bedrock/us.openai.gpt-6.1-sol` is `gpt-6.1-sol`)."""
|
||||
names: Final = (
|
||||
(row.id,)
|
||||
if row.upstream_model is None
|
||||
else (row.id, row.upstream_model, _provider_model_name(row.upstream_model, row.upstream_provider))
|
||||
)
|
||||
return next((stock[name] for name in names if name in stock), None)
|
||||
|
||||
|
||||
def _readable_by(stock_model: CodexStockModel, client_version: str | None) -> bool:
|
||||
"""Whether Codex's own server would hand this entry to the requesting release: one at or past the entry's
|
||||
`minimal_client_version`. An older release can fail to decode it, and one entry it cannot decode (a reasoning
|
||||
effort it has no name for) fails the whole catalog, so it gets a translated entry. No version keeps the entry."""
|
||||
minimum: Final = stock_model.minimal_client_version
|
||||
if minimum is None or client_version is None:
|
||||
return True
|
||||
try:
|
||||
return client_version is not None and Version(client_version) >= _FIRST_CODEX_WITH_CUSTOM_EFFORTS
|
||||
return Version(client_version) >= Version(minimum if isinstance(minimum, str) else ".".join(map(str, minimum)))
|
||||
except ValueError:
|
||||
return False
|
||||
|
||||
|
||||
def codex_reasoning_levels(
|
||||
efforts: Sequence[str], client_version: str | None
|
||||
) -> tuple[tuple[CodexFallbackReasoningLevel, ...], str | None]:
|
||||
"""The reasoning levels a fallback entry offers for a model group's efforts, and its default level.
|
||||
def _tier_fields(stock_model: CodexStockModel, tiers: tuple[CodexServiceTier, ...]) -> Mapping[str, object]:
|
||||
"""The tier fields of an entry offering `tiers`. Codex turns `/fast` on off a speed tier as much as off a
|
||||
service tier, so a speed tier stays only while a service tier billed the same way is still offered, and the
|
||||
default tier only while it is still offered."""
|
||||
from litellm.litellm_core_utils.llm_cost_calc.utils import get_service_tier_cost_key
|
||||
|
||||
Codex before 0.138.0 fails the whole catalog on an effort it has no name for, so for it, and for
|
||||
a version that does not parse, only the efforts every release knows are offered. The default is
|
||||
set whenever a level is offered, because Codex reads a missing one as `none`: `none` itself when the
|
||||
group accepts it, so a user who picks no level sends what Codex sent before, else medium, else the
|
||||
nearest weaker declared level, so picking no level never costs more reasoning than Codex's usual medium,
|
||||
else the weakest declared level, else the first one."""
|
||||
parses_any: Final = _parses_custom_efforts(client_version)
|
||||
offered: Final = tuple(effort for effort in efforts if parses_any or effort in _EFFORTS_EVERY_CODEX_PARSES)
|
||||
if not offered:
|
||||
return (), None
|
||||
nearest: Final = nearest_declared_reasoning_effort(_PREFERRED_DEFAULT_EFFORT, offered, "down")
|
||||
levels: Final = tuple(
|
||||
CodexFallbackReasoningLevel(effort=effort, description=f"Sends reasoning_effort={effort} upstream")
|
||||
for effort in offered
|
||||
offered: Final = frozenset(tier.id for tier in tiers)
|
||||
offered_prices: Final = frozenset(get_service_tier_cost_key(TOKEN_PRICE_KEY, tier) for tier in offered)
|
||||
speed_tiers: Final = tuple(
|
||||
tier
|
||||
for tier in stock_model.additional_speed_tiers
|
||||
if get_service_tier_cost_key(TOKEN_PRICE_KEY, tier) in offered_prices - {TOKEN_PRICE_KEY}
|
||||
)
|
||||
return MappingProxyType(
|
||||
{
|
||||
"service_tiers": tiers,
|
||||
**({"additional_speed_tiers": speed_tiers} if stock_model.additional_speed_tiers else {}),
|
||||
"default_service_tier": (
|
||||
stock_model.default_service_tier if stock_model.default_service_tier in offered else None
|
||||
),
|
||||
}
|
||||
)
|
||||
if _CODEX_UNSET_EFFORT in offered:
|
||||
return levels, _CODEX_UNSET_EFFORT
|
||||
return levels, nearest if nearest in offered else offered[0]
|
||||
|
||||
|
||||
def _stock_for(row: CodexCatalogRow, stock: Mapping[str, CodexStockModel]) -> CodexStockModel | None:
|
||||
listed: Final = stock.get(row.id)
|
||||
if listed is not None or row.upstream_model is None:
|
||||
return listed
|
||||
upstream: Final = stock.get(row.upstream_model)
|
||||
return upstream if upstream is not None else stock.get(row.upstream_model.partition("/")[2])
|
||||
def _priced_tiers(row: CodexCatalogRow, stock_model: CodexStockModel) -> Mapping[str, object]:
|
||||
"""Codex's own tiers for the model without the ones some deployment behind the row prices tokens but not
|
||||
that tier for, or nothing when none is dropped. Sending a tier the upstream does not sell fails the
|
||||
request, and the cost map prices a tier for exactly the models that sell it, while a deployment it has no
|
||||
token price for at all says nothing either way."""
|
||||
deployments: Final = row.deployment_capabilities
|
||||
tiers: Final = tuple(
|
||||
tier for tier in stock_model.service_tiers if no_priced_deployment_lacks_tier(tier.id, deployments)
|
||||
)
|
||||
return _NO_UPDATE if tiers == stock_model.service_tiers else _tier_fields(stock_model, tiers)
|
||||
|
||||
|
||||
def _alias_context_window(row: CodexCatalogRow, stock_model: CodexStockModel) -> Mapping[str, int]:
|
||||
|
|
@ -369,8 +366,7 @@ def _deployment_defaults(row: CodexCatalogRow, stock_model: CodexStockModel) ->
|
|||
none otherwise, and a window no larger than the alias's own limit."""
|
||||
return MappingProxyType(
|
||||
{
|
||||
"service_tiers": (),
|
||||
"default_service_tier": None,
|
||||
**_tier_fields(stock_model, ()),
|
||||
"default_verbosity": None,
|
||||
"upgrade": None,
|
||||
"availability_nux": None,
|
||||
|
|
@ -389,24 +385,24 @@ def _entry(
|
|||
alias_defaults: CodexAliasDefaults,
|
||||
client_version: str | None,
|
||||
) -> CodexCatalogEntry:
|
||||
stock_model: Final = _stock_for(row, stock)
|
||||
matched: Final = _stock_for(row, stock)
|
||||
stock_model: Final = matched if matched is not None and _readable_by(matched, client_version) else None
|
||||
stock_tiers: Final = MappingProxyType(
|
||||
{tier.id: tier for tier in (stock_model.service_tiers if stock_model is not None else ())}
|
||||
)
|
||||
configured: Final = configured_service_tiers(row.service_tiers, row.id, stock_tiers)
|
||||
search_tool: Final = offered_search_tool(row.search_tool_support)
|
||||
if stock_model is None:
|
||||
levels, default_level = codex_reasoning_levels(row.supported_reasoning_efforts or (), client_version)
|
||||
return CodexFallbackModel(
|
||||
return codex_fallback_entry(
|
||||
slug=row.id,
|
||||
display_name=row.display_name if row.display_name is not None else row.id,
|
||||
default_reasoning_level=default_level,
|
||||
supported_reasoning_levels=levels,
|
||||
priority=index,
|
||||
capabilities=combined_capabilities(row.deployment_capabilities, row.max_input_tokens),
|
||||
instructions=instructions,
|
||||
service_tiers=configured if configured is not None else (),
|
||||
context_window=row.max_input_tokens,
|
||||
base_instructions=instructions,
|
||||
supports_search_tool=search_tool is True,
|
||||
reasoning_efforts=row.supported_reasoning_efforts or (),
|
||||
client_version=client_version,
|
||||
)
|
||||
upgrade: Final = (
|
||||
stock_model.upgrade if stock_model.upgrade is not None and stock_model.upgrade.model in served else None
|
||||
|
|
@ -425,17 +421,8 @@ def _entry(
|
|||
**_configured_window(row, stock_model),
|
||||
}
|
||||
if configured is None:
|
||||
return stock_model.model_copy(update=listing)
|
||||
configured_ids: Final = frozenset(tier.id for tier in configured)
|
||||
return stock_model.model_copy(
|
||||
update={
|
||||
**listing,
|
||||
"service_tiers": configured,
|
||||
"default_service_tier": (
|
||||
stock_model.default_service_tier if stock_model.default_service_tier in configured_ids else None
|
||||
),
|
||||
}
|
||||
)
|
||||
return stock_model.model_copy(update={**_priced_tiers(row, stock_model), **listing})
|
||||
return stock_model.model_copy(update={**listing, **_tier_fields(stock_model, configured)})
|
||||
|
||||
|
||||
def _serialized(entry: CodexCatalogEntry) -> str:
|
||||
|
|
@ -503,8 +490,10 @@ def codex_models_response_json(
|
|||
`max_context_window`. A row listed under another name than the Codex model its `upstream_model` names (an alias) takes that model's
|
||||
entry; `alias_defaults="deployment"` drops the defaults Codex picks for that model in favor of
|
||||
what the alias's deployments pin, while `"codex"` keeps them, as Codex would use them for the
|
||||
model's own name. `client_version` is the requesting Codex's version, which decides the reasoning
|
||||
levels a fallback entry may offer; None counts as a version too old for custom levels.
|
||||
model's own name. Codex's own service tiers stay on a known model's entry only when no deployment
|
||||
behind the row prices tokens without pricing them, and only while the operator configures none. `client_version` is the
|
||||
requesting Codex's version, which decides the reasoning levels a fallback entry may offer; None counts
|
||||
as a version too old for custom levels.
|
||||
"""
|
||||
known: Final = bundled_codex_models() if stock is None else stock
|
||||
prompt: Final = codex_base_instructions() if instructions is None else instructions
|
||||
|
|
@ -531,6 +520,35 @@ def _group_reasoning_efforts(
|
|||
return group.supported_reasoning_efforts if group is not None else None
|
||||
|
||||
|
||||
def deployment_capabilities(model_info: Mapping[str, object] | None, model_id: str) -> CodexDeploymentCapabilities:
|
||||
"""One deployment's model info, merged with the cost map, as the capabilities a Codex entry describes.
|
||||
Model info that does not validate declares nothing, logged, so one bad value never fails the listing."""
|
||||
if model_info is None:
|
||||
return CodexDeploymentCapabilities()
|
||||
prices_tokens: Final = model_info.get(TOKEN_PRICE_KEY) is not None
|
||||
priced: Final = frozenset(key for key in tier_price_keys() if model_info.get(key) is not None)
|
||||
try:
|
||||
return CodexDeploymentCapabilities.model_validate(
|
||||
{**model_info, "prices_tokens": prices_tokens, "priced_service_tier_keys": priced}
|
||||
)
|
||||
except ValidationError as e:
|
||||
verbose_proxy_logger.warning(
|
||||
"model info of a deployment of %s declares no capability to Codex: %s", model_id, e.errors()[0]["msg"]
|
||||
)
|
||||
return CodexDeploymentCapabilities(prices_tokens=prices_tokens, priced_service_tier_keys=priced)
|
||||
|
||||
|
||||
def _deployment_capabilities(
|
||||
lookup_id: str, registry: RouterRegistry | None, team_id: str | None
|
||||
) -> tuple[CodexDeploymentCapabilities, ...]:
|
||||
if registry is None:
|
||||
return ()
|
||||
return tuple(
|
||||
deployment_capabilities(model_info, lookup_id)
|
||||
for model_info in registry.group(lookup_id, field="deployment_model_info", team_id=team_id)
|
||||
)
|
||||
|
||||
|
||||
def _catalog_row(
|
||||
row: ModelInfoResponse, lookup_id: str, label_id: str, llm_router: Router | None, team_id: str | None
|
||||
) -> CodexCatalogRow:
|
||||
|
|
@ -540,6 +558,9 @@ def _catalog_row(
|
|||
mode=row.get("mode"),
|
||||
max_input_tokens=row.get("max_input_tokens"),
|
||||
upstream_model=registry.resolve(lookup_id, kind="upstream", team_id=team_id) if registry is not None else None,
|
||||
upstream_provider=(
|
||||
registry.resolve(lookup_id, kind="upstream_provider", team_id=team_id) if registry is not None else None
|
||||
),
|
||||
display_name=registry.group(label_id, field="display_name") if registry is not None else None,
|
||||
service_tiers=registry.group(lookup_id, field="service_tiers", team_id=team_id) if registry is not None else (),
|
||||
supported_reasoning_efforts=_group_reasoning_efforts(row, lookup_id, registry, team_id),
|
||||
|
|
@ -549,6 +570,7 @@ def _catalog_row(
|
|||
configured_max_input_tokens=(
|
||||
registry.group(lookup_id, field="max_input_tokens", team_id=team_id) if registry is not None else ()
|
||||
),
|
||||
deployment_capabilities=_deployment_capabilities(lookup_id, registry, team_id),
|
||||
)
|
||||
|
||||
|
||||
|
|
@ -562,8 +584,9 @@ def codex_catalog_rows(
|
|||
"""`rows` joined with the router's configured metadata, looked up by each entry's internal id so
|
||||
team-scoped rows resolve the way the Anthropic listing's display names do, a `model_group_alias`
|
||||
reading its target's deployments under its own name; `team_id` is the requesting key's team, so the
|
||||
upstream model that picks Codex's stock entry, the tiers, and the configured input limits are read
|
||||
only off the deployments its requests can route to, and the reasoning efforts are the ones
|
||||
upstream model that picks Codex's stock entry, the tiers, the configured input limits and each
|
||||
deployment's capabilities are read only off the deployments its requests can route to, and the
|
||||
reasoning efforts are the ones
|
||||
`/model_group/info` reports for the entry. Display names are read under `label_entries`' ids when given."""
|
||||
lookup_ids: Final = MappingProxyType(dict(entries))
|
||||
label_ids: Final = lookup_ids if label_entries is None else MappingProxyType(dict(label_entries))
|
||||
|
|
|
|||
250
litellm/proxy/common_utils/codex_model_translation.py
Normal file
250
litellm/proxy/common_utils/codex_model_translation.py
Normal file
|
|
@ -0,0 +1,250 @@
|
|||
"""Codex's `ModelInfo` entry for a model Codex does not know, translated from what LiteLLM knows about the
|
||||
deployments behind it.
|
||||
|
||||
Field names, types and enum values follow `codex-rs/protocol/src/openai_models.rs` at rust-v0.166.0, and every
|
||||
field a release back to 0.140.0 decodes without a default is spelled out, so one body parses on all of them.
|
||||
Codex rejects the whole catalog when one entry fails to decode, so only enum values every one of those
|
||||
releases knows are sent.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from collections.abc import Iterable, Sequence
|
||||
from dataclasses import dataclass
|
||||
from functools import cache
|
||||
from types import NoneType
|
||||
from typing import Annotated, Final, Literal, TypeAlias
|
||||
|
||||
from packaging.version import Version
|
||||
from pydantic import ConfigDict, Field, StringConstraints
|
||||
|
||||
from litellm.litellm_core_utils.reasoning_effort_strength import nearest_declared_reasoning_effort
|
||||
from litellm.types.llms.base_model import LiteLLMBaseModel
|
||||
|
||||
TierId: TypeAlias = Annotated[str, StringConstraints(strip_whitespace=True, min_length=1)]
|
||||
CodexInputModality: TypeAlias = Literal["text", "image"]
|
||||
|
||||
_TEXT_ONLY: Final[tuple[CodexInputModality, ...]] = ("text",)
|
||||
_TEXT_AND_IMAGE: Final[tuple[CodexInputModality, ...]] = ("text", "image")
|
||||
TOKEN_PRICE_KEY: Final = "input_cost_per_token"
|
||||
_FIRST_CODEX_WITH_CUSTOM_EFFORTS: Final = Version("0.138.0")
|
||||
_EFFORTS_EVERY_CODEX_PARSES: Final = frozenset({"none", "minimal", "low", "medium", "high", "xhigh"})
|
||||
_PREFERRED_DEFAULT_EFFORT: Final = "medium"
|
||||
_CODEX_UNSET_EFFORT: Final = "none"
|
||||
|
||||
|
||||
def _unset(value: object) -> bool:
|
||||
return value is None
|
||||
|
||||
|
||||
class CodexServiceTier(LiteLLMBaseModel):
|
||||
"""A `ModelServiceTier` as Codex reads it: the slash command is `name` lowercased, and toggling
|
||||
it sends `id` as the request's `service_tier`."""
|
||||
|
||||
model_config = ConfigDict(frozen=True)
|
||||
|
||||
id: TierId
|
||||
name: str
|
||||
description: str
|
||||
|
||||
|
||||
class CodexFallbackReasoningLevel(LiteLLMBaseModel):
|
||||
"""A `ReasoningEffortPreset` as Codex reads it: picking it sends `effort` as the request's reasoning effort."""
|
||||
|
||||
model_config = ConfigDict(frozen=True)
|
||||
|
||||
effort: str
|
||||
description: str
|
||||
|
||||
|
||||
class CodexTruncationPolicy(LiteLLMBaseModel):
|
||||
"""How Codex truncates a long tool output before sending it back, the policy its own entries use."""
|
||||
|
||||
model_config = ConfigDict(frozen=True)
|
||||
|
||||
mode: Literal["tokens"] = "tokens"
|
||||
limit: int = 10_000
|
||||
|
||||
|
||||
class CodexFallbackModel(LiteLLMBaseModel):
|
||||
"""One `ModelInfo` entry of a Codex model catalog for a model Codex does not know.
|
||||
|
||||
`supports_reasoning_summaries` and `supports_parallel_tool_calls` are read only by Codex before 0.150.0,
|
||||
which requires them, and `base_instructions` is the prompt field every release still decodes.
|
||||
"""
|
||||
|
||||
model_config = ConfigDict(frozen=True)
|
||||
|
||||
slug: str
|
||||
display_name: str
|
||||
description: NoneType = None
|
||||
default_reasoning_level: str | None = Field(default=None, exclude_if=_unset)
|
||||
supported_reasoning_levels: tuple[CodexFallbackReasoningLevel, ...] = ()
|
||||
shell_type: Literal["unified_exec"] = "unified_exec"
|
||||
visibility: Literal["list"] = "list"
|
||||
supported_in_api: Literal[True] = True
|
||||
priority: int
|
||||
service_tiers: tuple[CodexServiceTier, ...] = ()
|
||||
availability_nux: NoneType = None
|
||||
upgrade: NoneType = None
|
||||
support_verbosity: Literal[False] = False
|
||||
supports_reasoning_summary_parameter: bool | None = Field(default=None, exclude_if=_unset)
|
||||
supports_reasoning_summaries: bool = False
|
||||
supports_parallel_tool_calls: bool = False
|
||||
default_verbosity: NoneType = None
|
||||
apply_patch_tool_type: Literal["freeform"] | None = None
|
||||
truncation_policy: CodexTruncationPolicy = CodexTruncationPolicy()
|
||||
experimental_supported_tools: tuple[()] = ()
|
||||
input_modalities: tuple[CodexInputModality, ...] = _TEXT_AND_IMAGE
|
||||
context_window: int | None
|
||||
base_instructions: str
|
||||
supports_search_tool: bool = False
|
||||
use_responses_lite: Literal[False] = False
|
||||
|
||||
|
||||
class CodexDeploymentCapabilities(LiteLLMBaseModel):
|
||||
"""What one deployment's model info says about the capabilities a Codex entry describes, None where
|
||||
it says nothing, whether it has a token price at all, and the input price keys of the service tiers it
|
||||
has a price for."""
|
||||
|
||||
model_config = ConfigDict(frozen=True, extra="ignore")
|
||||
|
||||
max_input_tokens: int | None = None
|
||||
supports_vision: bool | None = None
|
||||
supports_function_calling: bool | None = None
|
||||
supports_parallel_function_calling: bool | None = None
|
||||
supports_reasoning: bool | None = None
|
||||
prices_tokens: bool = False
|
||||
priced_service_tier_keys: frozenset[str] = frozenset()
|
||||
|
||||
|
||||
@cache
|
||||
def tier_price_keys() -> tuple[str, ...]:
|
||||
"""The input price key of every service tier cost tracking prices. Cost tracking is imported on first use
|
||||
because `lite codex` imports this module before the SDK is initialized, and cost tracking imports the rest
|
||||
of the SDK."""
|
||||
from litellm.litellm_core_utils.llm_cost_calc.utils import SERVICE_TIER_COST_KEY_SUFFIXES
|
||||
|
||||
return tuple(f"{TOKEN_PRICE_KEY}{suffix}" for suffix in SERVICE_TIER_COST_KEY_SUFFIXES)
|
||||
|
||||
|
||||
def no_priced_deployment_lacks_tier(tier_id: str, deployments: Sequence[CodexDeploymentCapabilities]) -> bool:
|
||||
"""Whether every deployment that has a token price also has one for `tier_id`, the way cost tracking
|
||||
prices a request sent at that tier. A deployment with no token price at all (a subscription upstream,
|
||||
a deployment name the cost map does not know) says nothing about tiers, so it does not vote, and a tier
|
||||
cost tracking has no price key for is lacked by every deployment that prices tokens."""
|
||||
from litellm.litellm_core_utils.llm_cost_calc.utils import get_service_tier_cost_key
|
||||
|
||||
price_key: Final = get_service_tier_cost_key(TOKEN_PRICE_KEY, tier_id)
|
||||
return all(
|
||||
price_key in deployment.priced_service_tier_keys for deployment in deployments if deployment.prices_tokens
|
||||
)
|
||||
|
||||
|
||||
@dataclass(frozen=True, slots=True)
|
||||
class CodexCapabilities:
|
||||
"""What a request to a listed name can count on whichever deployment serves it: a capability is False
|
||||
when one deployment lacks it, True when one has it and none lacks it, None when none says, and the
|
||||
context window is the smallest one any deployment declares."""
|
||||
|
||||
context_window: int | None = None
|
||||
supports_vision: bool | None = None
|
||||
supports_function_calling: bool | None = None
|
||||
supports_parallel_function_calling: bool | None = None
|
||||
supports_reasoning: bool | None = None
|
||||
|
||||
|
||||
def _combined(flags: Iterable[bool | None]) -> bool | None:
|
||||
said: Final = frozenset(flag for flag in flags if flag is not None)
|
||||
return None if not said else False not in said
|
||||
|
||||
|
||||
def combined_capabilities(
|
||||
deployments: Sequence[CodexDeploymentCapabilities], listed_context_window: int | None = None
|
||||
) -> CodexCapabilities:
|
||||
"""The capabilities every one of `deployments` offers, the window falling back to
|
||||
`listed_context_window` when no deployment declares one."""
|
||||
windows: Final = tuple(
|
||||
deployment.max_input_tokens
|
||||
for deployment in deployments
|
||||
if deployment.max_input_tokens is not None and deployment.max_input_tokens > 0
|
||||
)
|
||||
return CodexCapabilities(
|
||||
context_window=min(windows) if windows else listed_context_window,
|
||||
supports_vision=_combined(deployment.supports_vision for deployment in deployments),
|
||||
supports_function_calling=_combined(deployment.supports_function_calling for deployment in deployments),
|
||||
supports_parallel_function_calling=_combined(
|
||||
deployment.supports_parallel_function_calling for deployment in deployments
|
||||
),
|
||||
supports_reasoning=_combined(deployment.supports_reasoning for deployment in deployments),
|
||||
)
|
||||
|
||||
|
||||
def _parses_custom_efforts(client_version: str | None) -> bool:
|
||||
try:
|
||||
return client_version is not None and Version(client_version) >= _FIRST_CODEX_WITH_CUSTOM_EFFORTS
|
||||
except ValueError:
|
||||
return False
|
||||
|
||||
|
||||
def codex_reasoning_levels(
|
||||
efforts: Sequence[str], client_version: str | None
|
||||
) -> tuple[tuple[CodexFallbackReasoningLevel, ...], str | None]:
|
||||
"""The reasoning levels a fallback entry offers for a model group's efforts, and its default level.
|
||||
|
||||
Codex before 0.138.0 fails the whole catalog on an effort it has no name for, so for it, and for
|
||||
a version that does not parse, only the efforts every release knows are offered. The default is
|
||||
set whenever a level is offered, because Codex reads a missing one as `none`: `none` itself when the
|
||||
group accepts it, so a user who picks no level sends what Codex sent before, else medium, else the
|
||||
nearest weaker declared level, so picking no level never costs more reasoning than Codex's usual medium,
|
||||
else the weakest declared level, else the first one."""
|
||||
parses_any: Final = _parses_custom_efforts(client_version)
|
||||
offered: Final = tuple(effort for effort in efforts if parses_any or effort in _EFFORTS_EVERY_CODEX_PARSES)
|
||||
if not offered:
|
||||
return (), None
|
||||
nearest: Final = nearest_declared_reasoning_effort(_PREFERRED_DEFAULT_EFFORT, offered, "down")
|
||||
levels: Final = tuple(
|
||||
CodexFallbackReasoningLevel(effort=effort, description=f"Sends reasoning_effort={effort} upstream")
|
||||
for effort in offered
|
||||
)
|
||||
if _CODEX_UNSET_EFFORT in offered:
|
||||
return levels, _CODEX_UNSET_EFFORT
|
||||
return levels, nearest if nearest in offered else offered[0]
|
||||
|
||||
|
||||
def codex_fallback_entry(
|
||||
*,
|
||||
slug: str,
|
||||
display_name: str,
|
||||
priority: int,
|
||||
capabilities: CodexCapabilities,
|
||||
instructions: str,
|
||||
service_tiers: tuple[CodexServiceTier, ...] = (),
|
||||
supports_search_tool: bool = False,
|
||||
reasoning_efforts: Sequence[str] = (),
|
||||
client_version: str | None = None,
|
||||
) -> CodexFallbackModel:
|
||||
"""Codex's entry for a model it does not know, from the capabilities its deployments share.
|
||||
|
||||
A capability no deployment declares keeps Codex's own default for an unknown model, except the freeform
|
||||
`apply_patch` tool, which goes out only to a model that calls tools: `supports_reasoning_summary_parameter`
|
||||
is left out, which Codex reads as true, and the fields only Codex before 0.150.0 reads keep the false
|
||||
it requires.
|
||||
"""
|
||||
levels, default_level = codex_reasoning_levels(reasoning_efforts, client_version)
|
||||
return CodexFallbackModel(
|
||||
slug=slug,
|
||||
display_name=display_name,
|
||||
default_reasoning_level=default_level,
|
||||
supported_reasoning_levels=levels,
|
||||
priority=priority,
|
||||
service_tiers=service_tiers,
|
||||
supports_reasoning_summary_parameter=capabilities.supports_reasoning,
|
||||
supports_reasoning_summaries=capabilities.supports_reasoning is True,
|
||||
supports_parallel_tool_calls=capabilities.supports_parallel_function_calling is True,
|
||||
apply_patch_tool_type="freeform" if capabilities.supports_function_calling is True else None,
|
||||
input_modalities=_TEXT_ONLY if capabilities.supports_vision is False else _TEXT_AND_IMAGE,
|
||||
context_window=capabilities.context_window,
|
||||
base_instructions=instructions,
|
||||
supports_search_tool=supports_search_tool,
|
||||
)
|
||||
|
|
@ -145,6 +145,7 @@ ResolveKind: TypeAlias = Literal[
|
|||
"team_model",
|
||||
"recognized",
|
||||
"upstream",
|
||||
"upstream_provider",
|
||||
"group_of_id",
|
||||
"wildcard_access_group",
|
||||
"team_model_alternatives",
|
||||
|
|
@ -158,6 +159,7 @@ GroupField: TypeAlias = Literal[
|
|||
"mode",
|
||||
"service_tiers",
|
||||
"max_input_tokens",
|
||||
"deployment_model_info",
|
||||
"token_limits",
|
||||
"listing_info",
|
||||
]
|
||||
|
|
@ -757,7 +759,7 @@ class RouterRegistry:
|
|||
self,
|
||||
name: str,
|
||||
*,
|
||||
kind: Literal["alias", "upstream"],
|
||||
kind: Literal["alias", "upstream", "upstream_provider"],
|
||||
team_id: str | None = None,
|
||||
custom_llm_provider: str | None = None,
|
||||
served_model: str | None = None,
|
||||
|
|
@ -837,6 +839,8 @@ class RouterRegistry:
|
|||
- ``team_model``: a team's public name to its group (was ``map_team_model``; ``team_id`` required)
|
||||
- ``recognized``: whether the router knows the name (was ``is_recognized_model``)
|
||||
- ``upstream``: the upstream model a name routes to (was ``get_routable_upstream_model``)
|
||||
- ``upstream_provider``: the provider the deployment ``upstream`` reads declares, by its
|
||||
``custom_llm_provider`` or its model's ``provider/`` prefix
|
||||
- ``group_of_id``: the group of a deployment id (was ``get_model_group``)
|
||||
- ``wildcard_access_group``: whether an access group grants a wildcard route
|
||||
(was ``is_model_access_group_for_wildcard_route``)
|
||||
|
|
@ -866,10 +870,8 @@ class RouterRegistry:
|
|||
return name if served_model is None else r._core_resolve_served_model_name(name, served_model)
|
||||
if kind == "recognized":
|
||||
return r._core_is_recognized_model(name)
|
||||
if kind == "upstream":
|
||||
return r._core_get_routable_upstream_model(name, team_id=team_id)
|
||||
if kind == "search_tool_support":
|
||||
return r._core_get_search_tool_support(name, team_id=team_id)
|
||||
if kind in ("upstream", "upstream_provider", "search_tool_support"):
|
||||
return self._resolve_routable(name, kind, team_id)
|
||||
if kind == "group_of_id":
|
||||
return r._core_get_model_group(id=name)
|
||||
if kind == "wildcard_access_group":
|
||||
|
|
@ -878,6 +880,19 @@ class RouterRegistry:
|
|||
return r._core_team_model_has_alternatives(name)
|
||||
raise ValueError(f"unknown resolve kind {kind!r}")
|
||||
|
||||
def _resolve_routable(
|
||||
self,
|
||||
name: str,
|
||||
kind: Literal["upstream", "upstream_provider", "search_tool_support"],
|
||||
team_id: str | None,
|
||||
) -> str | tuple[bool | None, ...] | None:
|
||||
r: Final = self._router
|
||||
if kind == "upstream":
|
||||
return r._core_get_routable_upstream_model(name, team_id=team_id)
|
||||
if kind == "upstream_provider":
|
||||
return r._core_get_routable_upstream_provider(name, team_id=team_id)
|
||||
return r._core_get_search_tool_support(name, team_id=team_id)
|
||||
|
||||
def aliases(
|
||||
self,
|
||||
) -> "dict[str, str | RouterModelGroupAliasItem]": # mutable-ok: the model_group_alias attribute, the live dict
|
||||
|
|
@ -939,6 +954,16 @@ class RouterRegistry:
|
|||
team_id: str | None = None,
|
||||
) -> tuple[int | None, ...]: ...
|
||||
|
||||
@overload
|
||||
def group(
|
||||
self,
|
||||
name: str,
|
||||
*,
|
||||
field: Literal["deployment_model_info"],
|
||||
include_hidden: bool = False,
|
||||
team_id: str | None = None,
|
||||
) -> "tuple[ModelMapInfo | None, ...]": ...
|
||||
|
||||
@overload
|
||||
def group(
|
||||
self,
|
||||
|
|
@ -977,6 +1002,7 @@ class RouterRegistry:
|
|||
``get_configured_token_limits``, ``get_model_listing_info``)
|
||||
- ``service_tiers``: (was ``get_configured_service_tiers``)
|
||||
- ``max_input_tokens``: each routable deployment's own ``model_info.max_input_tokens``
|
||||
- ``deployment_model_info``: each routable deployment's model info, merged with the pricing map
|
||||
"""
|
||||
r: Final = self._router
|
||||
if field == "info":
|
||||
|
|
@ -991,6 +1017,8 @@ class RouterRegistry:
|
|||
return r._core_get_configured_service_tiers(name, team_id=team_id)
|
||||
if field == "max_input_tokens":
|
||||
return r._core_get_configured_max_input_tokens(name, team_id=team_id)
|
||||
if field == "deployment_model_info":
|
||||
return r._core_get_routable_deployment_model_info(name, team_id=team_id)
|
||||
if field == "token_limits":
|
||||
return r._core_get_configured_token_limits(name)
|
||||
if field == "listing_info":
|
||||
|
|
|
|||
|
|
@ -65,6 +65,7 @@ from litellm.litellm_core_utils.core_helpers import as_str_mapping, without
|
|||
from litellm.litellm_core_utils.credential_accessor import CredentialAccessor
|
||||
from litellm.litellm_core_utils.get_llm_provider_logic import (
|
||||
declared_authenticating_provider,
|
||||
declared_provider,
|
||||
is_registered_custom_provider,
|
||||
resolve_model_and_provider_for_metadata,
|
||||
)
|
||||
|
|
@ -141,6 +142,7 @@ if TYPE_CHECKING:
|
|||
from litellm.router_rust.steps.prerouting import PreRoutingStrategies
|
||||
from litellm.router_strategy.budget_limiter import RouterBudgetLimiting
|
||||
from litellm.rust_bridge._native import RouterCoreHandle
|
||||
from litellm.types.utils import ModelInfo as ModelMapInfo
|
||||
|
||||
# The PTU share teams and the team ceilings read so far.
|
||||
_PTUMemo: TypeAlias = tuple[
|
||||
|
|
@ -679,7 +681,7 @@ class HostRegistry(HostModelInfo):
|
|||
self._cached_get_model_group_info: Callable[[str], ModelGroupInfo | None] = lru_cache( # pyright: ignore[reportIncompatibleMethodOverride] # the per-router cache shadows the uncached method, as the core keeps it
|
||||
maxsize=DEFAULT_MAX_LRU_CACHE_SIZE
|
||||
)(self._core_get_model_group_info)
|
||||
self._core_cached_deployment_model_info: Callable[[str, str], object] = lru_cache(
|
||||
self._core_cached_deployment_model_info: Callable[[str, str], ModelMapInfo | None] = lru_cache(
|
||||
maxsize=DEFAULT_MAX_LRU_CACHE_SIZE
|
||||
)(self._core_get_deployment_model_info)
|
||||
self.cached_deployment_tracks_usage: Callable[[str, str], bool] = lru_cache(maxsize=DEFAULT_MAX_LRU_CACHE_SIZE)(
|
||||
|
|
@ -2079,6 +2081,38 @@ class HostRegistry(HostModelInfo):
|
|||
model: Final = _params_of(deployment).get("model")
|
||||
return None if model is None else str(model)
|
||||
|
||||
def _core_get_routable_upstream_provider(self, model_name: str, team_id: str | None = None) -> str | None:
|
||||
"""The provider the deployment ``_core_get_routable_upstream_model`` reads declares: its
|
||||
``litellm_params.custom_llm_provider``, else its model's ``provider/`` prefix. None when it declares neither."""
|
||||
deployment: Final = next(iter(self._routable_deployments(model_name, team_id)), None)
|
||||
if deployment is None:
|
||||
return None
|
||||
params: Final = _params_of(deployment)
|
||||
model: Final = params.get("model")
|
||||
provider: Final = params.get("custom_llm_provider")
|
||||
return declared_provider(
|
||||
model if isinstance(model, str) else None, provider if isinstance(provider, str) else None
|
||||
)
|
||||
|
||||
def _core_get_routable_deployment_model_info(
|
||||
self, model_name: str, team_id: str | None = None
|
||||
) -> "tuple[ModelMapInfo | None, ...]":
|
||||
"""Each deployment a request from ``team_id`` to ``model_name`` can route to, read one at a time the way
|
||||
cost tracking reads it: its upstream model's cost map entry merged with its own ``model_info``, None where
|
||||
neither is known. Nothing is merged across deployments, so a caller can tell a deployment that lacks a
|
||||
capability from one that says nothing."""
|
||||
return tuple(
|
||||
self._routable_deployment_model_info(deployment)
|
||||
for deployment in self._routable_deployments(model_name, team_id)
|
||||
)
|
||||
|
||||
def _routable_deployment_model_info(self, deployment: Mapping[str, object]) -> "ModelMapInfo | None":
|
||||
deployment_id: Final = _model_info_of(deployment).get("id")
|
||||
model: Final = _params_of(deployment).get("model")
|
||||
if not isinstance(deployment_id, str) or not isinstance(model, str):
|
||||
return None
|
||||
return self._core_cached_deployment_model_info(deployment_id, model)
|
||||
|
||||
def _core_map_team_model(
|
||||
self, team_model_name: str | None, team_id: str, custom_llm_provider: str | None = None
|
||||
) -> str | None:
|
||||
|
|
|
|||
|
|
@ -61,7 +61,7 @@ include = [
|
|||
"litellm/router_strategy/complexity_router/fuse_presets.json",
|
||||
"litellm/proxy/model_insights_tasks.json",
|
||||
"litellm/proxy/common_utils/codex_base_instructions.md",
|
||||
"litellm/proxy/common_utils/codex_bundled_models_0.159.3.json",
|
||||
"litellm/proxy/common_utils/codex_bundled_models_0.166.0.json",
|
||||
]
|
||||
exclude = [
|
||||
"litellm/proxy/_experimental/out",
|
||||
|
|
|
|||
|
|
@ -342,7 +342,7 @@ include = [
|
|||
"litellm/proxy/model_insights_tasks.json",
|
||||
"litellm/proxy/public_endpoints/whats_new.json",
|
||||
"litellm/proxy/common_utils/codex_base_instructions.md",
|
||||
"litellm/proxy/common_utils/codex_bundled_models_0.159.3.json",
|
||||
"litellm/proxy/common_utils/codex_bundled_models_0.166.0.json",
|
||||
{ path = "model_prices_and_context_window.json", format = "sdist" },
|
||||
]
|
||||
exclude = [
|
||||
|
|
|
|||
|
|
@ -43,7 +43,7 @@ RELOAD_MARGIN_SECONDS: Final = 5.0
|
|||
PROXY_WORKERS: Final = int(os.environ.get("INTEGRATION_PROXY_WORKERS", "1"))
|
||||
WORKER_SYNC_SECONDS: Final = 0.0 if PROXY_WORKERS == 1 else PROXY_CONFIG_RELOAD_INTERVAL_SECONDS + RELOAD_MARGIN_SECONDS
|
||||
ROOT: Final = Path(os.environ.get("INTEGRATION_PROXY_ROOT") or Path(__file__).resolve().parents[3])
|
||||
BUNDLED_STOCK_PATH: Final = ROOT / "litellm" / "proxy" / "common_utils" / "codex_bundled_models_0.159.3.json"
|
||||
BUNDLED_STOCK_PATH: Final = ROOT / "litellm" / "proxy" / "common_utils" / "codex_bundled_models_0.166.0.json"
|
||||
BASE_INSTRUCTIONS_PATH: Final = ROOT / "litellm" / "proxy" / "common_utils" / "codex_base_instructions.md"
|
||||
FRESH_CONNECTION: Final = {"Connection": "close"}
|
||||
ANTHROPIC_HEADERS: Final = {"anthropic-version": "2023-06-01"}
|
||||
|
|
|
|||
|
|
@ -130,7 +130,7 @@ def test_yaml_tiers_and_alias_in_owned_proxy(gateway: Gateway, tmp_path: Path) -
|
|||
|
||||
|
||||
ROOT: Final = Path(os.environ.get("INTEGRATION_PROXY_ROOT") or Path(__file__).resolve().parents[3])
|
||||
BUNDLED_STOCK_PATH: Final = ROOT / "litellm" / "proxy" / "common_utils" / "codex_bundled_models_0.159.3.json"
|
||||
BUNDLED_STOCK_PATH: Final = ROOT / "litellm" / "proxy" / "common_utils" / "codex_bundled_models_0.166.0.json"
|
||||
STOCK_UPSTREAM: Final = "gpt-5.5"
|
||||
|
||||
|
||||
|
|
|
|||
|
|
@ -11,6 +11,7 @@ from litellm.llms.bedrock.common_utils import(
|
|||
NativeOutputFallback,
|
||||
bedrock_native_output_is_opt_in,
|
||||
bedrock_strict_tools_fallback,
|
||||
bedrock_model_name_without_provider,
|
||||
deployment_model_info,
|
||||
extract_model_name_from_bedrock_arn,
|
||||
for_bedrock_json_tool_retry,
|
||||
|
|
@ -305,6 +306,26 @@ def test_legacy_mantle_route_prefix_stripped_for_cost_lookup():
|
|||
assert BedrockModelInfo.get_bedrock_route("bedrock/mantle/anthropic.claude-sonnet-5") == "mantle"
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
("model", "expected"),
|
||||
[
|
||||
("bedrock/us.openai.gpt-6.1-sol", "gpt-6.1-sol"),
|
||||
("global.openai.gpt-6.1-sol", "gpt-6.1-sol"),
|
||||
("bedrock/converse/eu.anthropic.claude-sonnet-5-5", "claude-sonnet-5-5"),
|
||||
("us-east-1/openai.gpt-6.1-sol", "gpt-6.1-sol"),
|
||||
("openai.gpt-5.5", "gpt-5.5"),
|
||||
("arn:aws:bedrock:us-east-1:123456789012:inference-profile/us.meta.llama4-scout-17b", "llama4-scout-17b"),
|
||||
("us.deepseek.r1-v1:0", "r1-v1:0"),
|
||||
("moonshotai.kimi-k2-thinking", "kimi-k2-thinking"),
|
||||
("bedrock/qwen.qwen3-32b-v1:0", "qwen3-32b-v1:0"),
|
||||
("gpt-5.5", "gpt-5.5"),
|
||||
("arn:aws:bedrock:us-east-1:123456789012:imported-model/abc123", "abc123"),
|
||||
],
|
||||
)
|
||||
def test_bedrock_model_name_without_provider_strips_routing_region_and_provider_prefixes_only(model, expected):
|
||||
assert bedrock_model_name_without_provider(model) == expected
|
||||
|
||||
|
||||
def test_output_config_effort_normalization_uses_model_info_ceiling(monkeypatch):
|
||||
import litellm.llms.bedrock.common_utils as mod
|
||||
|
||||
|
|
|
|||
|
|
@ -1037,10 +1037,11 @@ class TestCodexModelSync:
|
|||
assert entry["supported_in_api"] is True
|
||||
assert entry["shell_type"] == "unified_exec"
|
||||
assert entry["supported_reasoning_levels"] == []
|
||||
assert entry["truncation_policy"] == {"mode": "bytes", "limit": 10000}
|
||||
assert entry["truncation_policy"] == {"mode": "tokens", "limit": 10000}
|
||||
assert entry["experimental_supported_tools"] == []
|
||||
assert entry["support_verbosity"] is False
|
||||
assert entry["supports_reasoning_summaries"] is False
|
||||
assert "supports_reasoning_summary_parameter" not in entry
|
||||
assert entry["supports_parallel_tool_calls"] is False
|
||||
for nullable in ("description", "availability_nux", "upgrade", "default_verbosity", "apply_patch_tool_type"):
|
||||
assert nullable in entry and entry[nullable] is None
|
||||
|
|
|
|||
|
|
@ -1,5 +1,6 @@
|
|||
import json
|
||||
from collections.abc import Mapping
|
||||
from dataclasses import replace
|
||||
from types import MappingProxyType
|
||||
from typing import Final, Literal
|
||||
|
||||
|
|
@ -7,10 +8,10 @@ import pytest
|
|||
|
||||
import litellm
|
||||
from litellm.litellm_core_utils.get_model_cost_map import GetModelCostMap
|
||||
from litellm.litellm_core_utils.llm_cost_calc.utils import get_service_tier_cost_key
|
||||
from litellm.proxy.common_utils.codex_model_catalog import (
|
||||
CODEX_CATALOG_BYTE_LIMIT,
|
||||
CodexCatalogRow,
|
||||
CodexServiceTier,
|
||||
CodexStockModel,
|
||||
CodexStockUpgrade,
|
||||
bundled_codex_models,
|
||||
|
|
@ -18,13 +19,17 @@ from litellm.proxy.common_utils.codex_model_catalog import (
|
|||
codex_model_list_body,
|
||||
codex_models_response_json,
|
||||
configured_service_tiers,
|
||||
deployment_capabilities,
|
||||
)
|
||||
from litellm.proxy.common_utils.codex_model_translation import CodexDeploymentCapabilities, CodexServiceTier
|
||||
|
||||
_PROMPT = "base prompt"
|
||||
_FAST = {"id": "priority", "name": "Fast", "description": "2x speed, increased usage"}
|
||||
|
||||
|
||||
def _stock(slug, *, visibility="list", supported_in_api=True, upgrade=None, service_tiers=(_FAST,), default_tier=None):
|
||||
def _stock(
|
||||
slug, *, visibility="list", supported_in_api=True, upgrade=None, service_tiers=(_FAST,), default_tier=None
|
||||
):
|
||||
return CodexStockModel(
|
||||
slug=slug,
|
||||
display_name=slug.upper(),
|
||||
|
|
@ -33,6 +38,7 @@ def _stock(slug, *, visibility="list", supported_in_api=True, upgrade=None, serv
|
|||
supported_in_api=supported_in_api,
|
||||
upgrade=CodexStockUpgrade(model=upgrade) if upgrade else None,
|
||||
service_tiers=tuple(CodexServiceTier(**tier) for tier in service_tiers),
|
||||
additional_speed_tiers=("fast",) if service_tiers else (),
|
||||
default_service_tier=default_tier,
|
||||
model_messages={"instructions_template": f"{slug} prompt"},
|
||||
)
|
||||
|
|
@ -84,29 +90,10 @@ def test_body_is_exactly_codex_models_response():
|
|||
assert [entry["slug"] for entry in body["models"]] == ["gpt-6-astra"]
|
||||
|
||||
|
||||
def test_unknown_model_entry_carries_every_field_codex_deserializes_without_a_default():
|
||||
"""Codex 0.159.3 `codex-rs/protocol/src/openai_models.rs` ModelInfo, the fields with no
|
||||
`#[serde(default)]`, read on 2026-10-01; `base_instructions` because its `ModelsResponse`
|
||||
decoder rejects an entry with neither it nor `model_messages.instructions_template`."""
|
||||
def test_unknown_model_entry_takes_its_slug_window_and_prompt_from_the_row():
|
||||
"""The fields every Codex release decodes are pinned in test_codex_model_translation.py."""
|
||||
(entry,) = _models(_row("gpt-6-astra"))
|
||||
|
||||
assert entry.keys() >= {
|
||||
"slug",
|
||||
"display_name",
|
||||
"description",
|
||||
"supported_reasoning_levels",
|
||||
"shell_type",
|
||||
"visibility",
|
||||
"supported_in_api",
|
||||
"priority",
|
||||
"availability_nux",
|
||||
"upgrade",
|
||||
"support_verbosity",
|
||||
"default_verbosity",
|
||||
"apply_patch_tool_type",
|
||||
"truncation_policy",
|
||||
"experimental_supported_tools",
|
||||
}
|
||||
assert (entry["slug"], entry["display_name"], entry["visibility"], entry["supported_in_api"]) == (
|
||||
"gpt-6-astra",
|
||||
"gpt-6-astra",
|
||||
|
|
@ -116,25 +103,14 @@ def test_unknown_model_entry_carries_every_field_codex_deserializes_without_a_de
|
|||
assert (entry["context_window"], entry["base_instructions"], entry["service_tiers"]) == (272000, _PROMPT, [])
|
||||
|
||||
|
||||
_NO_REASONING_BODY = (
|
||||
'{"models":[{"slug":"gpt-6-astra","display_name":"gpt-6-astra","description":null,'
|
||||
'"supported_reasoning_levels":[],"shell_type":"unified_exec","visibility":"list","supported_in_api":true,'
|
||||
'"priority":0,"service_tiers":[],"availability_nux":null,"upgrade":null,"support_verbosity":false,'
|
||||
'"supports_reasoning_summaries":false,"supports_parallel_tool_calls":false,"default_verbosity":null,'
|
||||
'"apply_patch_tool_type":null,"truncation_policy":{"mode":"bytes","limit":10000},'
|
||||
'"experimental_supported_tools":[],"context_window":272000,"base_instructions":"base prompt",'
|
||||
'"supports_search_tool":false}]}'
|
||||
)
|
||||
|
||||
|
||||
@pytest.mark.parametrize("efforts", [None, ()], ids=["unknown", "non-reasoning"])
|
||||
@pytest.mark.parametrize("client_version", ["0.165.0", "0.120.0", None])
|
||||
def test_unknown_or_no_reasoning_efforts_keep_the_fallback_entry_byte_for_byte(efforts, client_version):
|
||||
"""The body an unknown model got before reasoning efforts reached the catalog."""
|
||||
assert (
|
||||
_body(_row("gpt-6-astra", supported_reasoning_efforts=efforts), client_version=client_version).json
|
||||
== _NO_REASONING_BODY
|
||||
)
|
||||
def test_unknown_or_no_reasoning_efforts_offer_no_reasoning_level(efforts, client_version):
|
||||
"""Codex reads a missing default level as `none`, so an entry offering no level sends no effort."""
|
||||
(entry,) = _models(_row("mystery", supported_reasoning_efforts=efforts), client_version=client_version)
|
||||
|
||||
assert entry["supported_reasoning_levels"] == []
|
||||
assert "default_reasoning_level" not in entry
|
||||
|
||||
|
||||
_ABSENT = object()
|
||||
|
|
@ -204,6 +180,51 @@ def test_a_model_codex_knows_keeps_its_own_reasoning_levels(levels):
|
|||
assert (entry["default_reasoning_level"], entry["supported_reasoning_levels"]) == ("medium", levels)
|
||||
|
||||
|
||||
@pytest.mark.parametrize("minimum", ["0.153.0", [0, 153, 0]], ids=["string", "parts"])
|
||||
@pytest.mark.parametrize(
|
||||
("client_version", "display_name", "levels"),
|
||||
[
|
||||
("0.153.0", "GPT-6.1 Terra", ["low", "max"]),
|
||||
("0.166.0", "GPT-6.1 Terra", ["low", "max"]),
|
||||
(None, "GPT-6.1 Terra", ["low", "max"]),
|
||||
("0.152.9", "gpt-6.1-terra", ["low", "max"]),
|
||||
("0.137.0", "gpt-6.1-terra", ["low"]),
|
||||
("not-a-version", "gpt-6.1-terra", ["low"]),
|
||||
],
|
||||
)
|
||||
def test_a_codex_older_than_an_entrys_minimal_version_gets_an_entry_it_can_decode(
|
||||
minimum, client_version, display_name, levels
|
||||
):
|
||||
"""Codex 0.137 rejects the whole catalog on an effort it has no name for, such as the `max` Codex's own
|
||||
gpt-6.1-terra entry offers, so a release older than the entry's `minimal_client_version` gets the translated
|
||||
entry, whose levels are trimmed to that release."""
|
||||
stock = CodexStockModel.model_validate(
|
||||
{
|
||||
"slug": "gpt-6.1-terra",
|
||||
"display_name": "GPT-6.1 Terra",
|
||||
"priority": 3,
|
||||
"visibility": "list",
|
||||
"minimal_client_version": minimum,
|
||||
"default_reasoning_level": "low",
|
||||
"supported_reasoning_levels": [{"effort": "low"}, {"effort": "max"}],
|
||||
}
|
||||
)
|
||||
|
||||
(entry,) = json.loads(
|
||||
codex_models_response_json(
|
||||
(_row("gpt-6.1-terra", supported_reasoning_efforts=("low", "max")),),
|
||||
stock={"gpt-6.1-terra": stock},
|
||||
instructions=_PROMPT,
|
||||
client_version=client_version,
|
||||
).json
|
||||
)["models"]
|
||||
|
||||
assert (entry["display_name"], [level["effort"] for level in entry["supported_reasoning_levels"]]) == (
|
||||
display_name,
|
||||
levels,
|
||||
)
|
||||
|
||||
|
||||
def test_known_model_keeps_codex_stock_entry_listed_under_the_served_id():
|
||||
(entry,) = _models(_row("gpt-daybreak"))
|
||||
|
||||
|
|
@ -516,6 +537,27 @@ def test_configured_service_tiers_replace_stock_tiers_and_a_known_id_keeps_its_c
|
|||
assert _tiers(entry) == expected
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
("rows", "alias_defaults"),
|
||||
[
|
||||
((_row("gpt-5.5", service_tiers=[]),), "codex"),
|
||||
((_row("gpt-5.5", service_tiers=["ultrafast"]),), "codex"),
|
||||
((_row("gpt-5.5", service_tiers=["priority"]),), "codex"),
|
||||
((_row("gpt-5.5"),), "codex"),
|
||||
((_row("team-55", upstream_model="gpt-5.5"),), "deployment"),
|
||||
((_row("team-55", upstream_model="gpt-5.5", service_tiers=["priority"]),), "deployment"),
|
||||
],
|
||||
)
|
||||
def test_codexs_fast_toggle_is_on_exactly_when_the_entry_offers_the_priority_tier(rows, alias_defaults):
|
||||
"""Codex shows `/fast` for a speed tier `fast` as well as for a `priority` service tier, but only sends
|
||||
the tier when `service_tiers` lists it (codex-rs `ModelPreset::supports_fast_mode` and
|
||||
`get_service_tier`, 0.166.0), so a leftover speed tier is a `/fast` toggle that does nothing."""
|
||||
(entry,) = _models(*rows, alias_defaults=alias_defaults)
|
||||
|
||||
offers_priority = "priority" in [tier["id"] for tier in entry["service_tiers"]]
|
||||
assert ("fast" in entry["additional_speed_tiers"]) is offers_priority
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
"invalid", ["ultrafast", [""], [" "], [1], [{"name": "no id"}], [{"id": "x", "bogus": 1}], {"id": "x"}]
|
||||
)
|
||||
|
|
@ -724,12 +766,13 @@ def test_catalog_rows_read_the_router_by_each_entry_lookup_id():
|
|||
|
||||
rows = codex_catalog_rows(listing, (("gpt-5.5-team", "model_name_team-1_c0ffee"), ("plain", "plain")), router)
|
||||
|
||||
assert rows == (
|
||||
assert tuple(replace(row, deployment_capabilities=()) for row in rows) == (
|
||||
CodexCatalogRow(
|
||||
id="gpt-5.5-team",
|
||||
mode="chat",
|
||||
max_input_tokens=7,
|
||||
upstream_model="openai/gpt-5.5",
|
||||
upstream_provider="openai",
|
||||
display_name="Team 5.5",
|
||||
service_tiers=(["ultrafast"], None),
|
||||
supported_reasoning_efforts=router.get_model_group_info(
|
||||
|
|
@ -741,11 +784,15 @@ def test_catalog_rows_read_the_router_by_each_entry_lookup_id():
|
|||
CodexCatalogRow(
|
||||
id="plain",
|
||||
upstream_model="openai/some-unmapped-model",
|
||||
upstream_provider="openai",
|
||||
service_tiers=(None,),
|
||||
search_tool_support=(None,),
|
||||
configured_max_input_tokens=(None,),
|
||||
),
|
||||
)
|
||||
catalog_window = GetModelCostMap.loaded_model_cost_map()["gpt-5.5"]["max_input_tokens"]
|
||||
assert [deployment.max_input_tokens for deployment in rows[0].deployment_capabilities] == [catalog_window] * 2
|
||||
assert rows[1].deployment_capabilities == (CodexDeploymentCapabilities(),)
|
||||
|
||||
|
||||
def test_catalog_rows_read_an_alias_off_its_target_under_the_alias_name():
|
||||
|
|
@ -765,11 +812,13 @@ def test_catalog_rows_read_an_alias_off_its_target_under_the_alias_name():
|
|||
|
||||
(row,) = codex_catalog_rows(listing, (("gpt-6", "gpt-6"),), router)
|
||||
|
||||
assert row == CodexCatalogRow(
|
||||
assert len(row.deployment_capabilities) == 1
|
||||
assert replace(row, deployment_capabilities=()) == CodexCatalogRow(
|
||||
id="gpt-6",
|
||||
mode="chat",
|
||||
max_input_tokens=9,
|
||||
upstream_model="openai/gpt-6-astra",
|
||||
upstream_provider="openai",
|
||||
service_tiers=(["ultrafast"],),
|
||||
supported_reasoning_efforts=router.get_model_group_info("gpt-6-astra").supported_reasoning_efforts,
|
||||
search_tool_support=(None,),
|
||||
|
|
@ -944,3 +993,292 @@ def test_codex_tool_search_is_offered_off_the_route_and_model_info_and_only_when
|
|||
"mixed": False,
|
||||
}
|
||||
assert models[0]["context_window"] == 1000000
|
||||
|
||||
|
||||
_SOL_SLUG: Final = next(
|
||||
slug for slug, model in bundled_codex_models().items() if model.default_service_tier is not None
|
||||
)
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
"upstream",
|
||||
[
|
||||
"bedrock/us.openai.{slug}",
|
||||
"bedrock/global.openai.{slug}",
|
||||
"bedrock/converse/us.openai.{slug}",
|
||||
"bedrock/openai.{slug}",
|
||||
"bedrock_mantle/openai.{slug}",
|
||||
"bedrock_mantle/us-east-1/openai.{slug}",
|
||||
"openai/{slug}",
|
||||
"{slug}",
|
||||
],
|
||||
)
|
||||
@pytest.mark.parametrize("slug", sorted(bundled_codex_models()))
|
||||
def test_a_model_codex_knows_keeps_codexs_entry_under_any_provider_prefix(upstream: str, slug: str) -> None:
|
||||
stock: Final = bundled_codex_models()[slug].model_dump(mode="json")
|
||||
|
||||
(entry,) = json.loads(
|
||||
codex_models_response_json(
|
||||
(_row("team-model", upstream_model=upstream.format(slug=slug)),), byte_limit=None
|
||||
).json
|
||||
)["models"]
|
||||
|
||||
assert entry["model_messages"] == stock["model_messages"]
|
||||
assert entry["supported_reasoning_levels"] == stock["supported_reasoning_levels"]
|
||||
|
||||
|
||||
@pytest.mark.parametrize("model", ["us.openai.{slug}", "converse/global.openai.{slug}"])
|
||||
def test_a_bedrock_deployment_naming_its_provider_apart_from_the_model_keeps_codexs_entry(model: str) -> None:
|
||||
router: Final = litellm.Router(
|
||||
model_list=[
|
||||
{
|
||||
"model_name": "team-model",
|
||||
"litellm_params": {
|
||||
"model": model.format(slug=_SOL_SLUG),
|
||||
"custom_llm_provider": "bedrock",
|
||||
"aws_region_name": "us-east-1",
|
||||
},
|
||||
}
|
||||
]
|
||||
)
|
||||
|
||||
entry: Final = _served(router, "team-model")["team-model"]
|
||||
|
||||
assert entry["model_messages"] == bundled_codex_models()[_SOL_SLUG].model_dump(mode="json")["model_messages"]
|
||||
|
||||
|
||||
def test_an_upstream_with_no_provider_prefix_matches_only_by_its_own_name() -> None:
|
||||
(unknown, known) = json.loads(
|
||||
codex_models_response_json(
|
||||
(
|
||||
_row("first", upstream_model=f"us.openai.{_SOL_SLUG}"),
|
||||
_row("second", upstream_model=_SOL_SLUG),
|
||||
),
|
||||
instructions=_PROMPT,
|
||||
).json
|
||||
)["models"]
|
||||
|
||||
assert "model_messages" not in unknown and unknown["base_instructions"] == _PROMPT
|
||||
assert known["model_messages"] == bundled_codex_models()[_SOL_SLUG].model_dump(mode="json")["model_messages"]
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
"upstream",
|
||||
["bedrock/us.openai.{slug}-mini", "bedrock/us.meta.{slug}x", "bedrock_mantle/openai.{slug}.1", "{slug}-0"],
|
||||
)
|
||||
def test_a_model_whose_name_only_starts_like_one_codex_knows_gets_a_translated_entry(upstream: str) -> None:
|
||||
(entry,) = json.loads(
|
||||
codex_models_response_json(
|
||||
(_row("team-model", upstream_model=upstream.format(slug=_SOL_SLUG)),), instructions=_PROMPT
|
||||
).json
|
||||
)["models"]
|
||||
|
||||
assert "model_messages" not in entry and entry["base_instructions"] == _PROMPT
|
||||
|
||||
|
||||
def _listing(*names: str, max_input_tokens: int | None = None) -> tuple[dict[str, object], ...]:
|
||||
return tuple(
|
||||
{
|
||||
"id": name,
|
||||
"object": "model",
|
||||
"created": 0,
|
||||
"owned_by": "x",
|
||||
"mode": "chat",
|
||||
"max_input_tokens": max_input_tokens,
|
||||
}
|
||||
for name in names
|
||||
)
|
||||
|
||||
|
||||
def _served(
|
||||
router: litellm.Router, *names: str, team_id: str | None = None, **listing: int
|
||||
) -> dict[str, Mapping[str, object]]:
|
||||
body: Final = codex_model_list_body(
|
||||
_listing(*names, **listing), tuple((name, name) for name in names), router, team_id, client_version="0.166.0"
|
||||
)
|
||||
return {entry["slug"]: entry for entry in json.loads(body)["models"]}
|
||||
|
||||
|
||||
def _vllm(name: str, host: str, **model_info: object) -> dict[str, object]:
|
||||
return {
|
||||
"model_name": name,
|
||||
"litellm_params": {"model": f"hosted_vllm/coder-{host}", "api_base": f"https://{host}.example/v1"},
|
||||
"model_info": model_info,
|
||||
}
|
||||
|
||||
|
||||
def test_a_text_only_deployment_takes_images_off_the_name_and_a_silent_one_does_not() -> None:
|
||||
router: Final = litellm.Router(
|
||||
model_list=[
|
||||
_vllm("mixed", "h0", supports_vision=True),
|
||||
_vllm("mixed", "h1", supports_vision=False),
|
||||
_vllm("vision-and-silent", "h2", supports_vision=True),
|
||||
_vllm("vision-and-silent", "h3"),
|
||||
]
|
||||
)
|
||||
|
||||
served: Final = _served(router, "mixed", "vision-and-silent")
|
||||
|
||||
assert served["mixed"]["input_modalities"] == ["text"]
|
||||
assert served["vision-and-silent"]["input_modalities"] == ["text", "image"]
|
||||
|
||||
|
||||
def test_a_silent_deployment_does_not_vote_against_tool_calls_but_one_without_them_does() -> None:
|
||||
router: Final = litellm.Router(
|
||||
model_list=[
|
||||
_vllm("tools-and-silent", "h4", supports_function_calling=True),
|
||||
_vllm("tools-and-silent", "h5"),
|
||||
_vllm("tools-and-none", "h6", supports_function_calling=True),
|
||||
_vllm("tools-and-none", "h7", supports_function_calling=False),
|
||||
]
|
||||
)
|
||||
|
||||
served: Final = _served(router, "tools-and-silent", "tools-and-none")
|
||||
|
||||
assert served["tools-and-silent"]["apply_patch_tool_type"] == "freeform"
|
||||
assert served["tools-and-none"]["apply_patch_tool_type"] is None
|
||||
|
||||
|
||||
def test_a_mixed_group_takes_the_smallest_window_any_deployment_declares() -> None:
|
||||
router: Final = litellm.Router(
|
||||
model_list=[
|
||||
_vllm("mixed", "h8", max_input_tokens=200_000),
|
||||
_vllm("mixed", "h9", max_input_tokens=128_000, supports_vision=True),
|
||||
_vllm("mixed", "h10"),
|
||||
_vllm("undeclared", "h11"),
|
||||
]
|
||||
)
|
||||
|
||||
served: Final = _served(router, "mixed", "undeclared", max_input_tokens=200_000)
|
||||
|
||||
assert served["mixed"]["context_window"] == 128_000
|
||||
assert served["undeclared"]["context_window"] == 200_000
|
||||
|
||||
|
||||
def test_a_claude_like_deployment_gets_codexs_coding_setup() -> None:
|
||||
router: Final = litellm.Router(
|
||||
model_list=[
|
||||
_vllm(
|
||||
"claude-like",
|
||||
"h-claude",
|
||||
max_input_tokens=1_000_000,
|
||||
supports_vision=True,
|
||||
supports_function_calling=True,
|
||||
supports_reasoning=True,
|
||||
),
|
||||
_vllm("plain", "h12"),
|
||||
_vllm("no-reasoning", "h12b", supports_reasoning=False),
|
||||
]
|
||||
)
|
||||
|
||||
served: Final = _served(router, "claude-like", "plain", "no-reasoning")
|
||||
|
||||
claude_like: Final = served["claude-like"]
|
||||
assert claude_like["apply_patch_tool_type"] == "freeform"
|
||||
assert claude_like["truncation_policy"] == {"mode": "tokens", "limit": 10000}
|
||||
assert claude_like["supports_reasoning_summary_parameter"] is True
|
||||
assert claude_like["context_window"] == 1_000_000
|
||||
assert served["plain"]["apply_patch_tool_type"] is None
|
||||
assert "supports_reasoning_summary_parameter" not in served["plain"]
|
||||
assert served["no-reasoning"]["supports_reasoning_summary_parameter"] is False
|
||||
|
||||
|
||||
def test_capabilities_are_read_only_off_the_deployments_the_key_team_routes_to() -> None:
|
||||
router: Final = litellm.Router(
|
||||
model_list=[
|
||||
_vllm("coder", "h13", supports_vision=True),
|
||||
_vllm("coder", "h14", supports_vision=False, team_id="team-2"),
|
||||
]
|
||||
)
|
||||
|
||||
assert _served(router, "coder", team_id="team-1")["coder"]["input_modalities"] == ["text", "image"]
|
||||
assert _served(router, "coder", team_id="team-2")["coder"]["input_modalities"] == ["text"]
|
||||
|
||||
|
||||
def _tier_fields(entry: Mapping[str, object]) -> tuple[object, object, object]:
|
||||
return (entry["service_tiers"], entry.get("additional_speed_tiers"), entry.get("default_service_tier"))
|
||||
|
||||
|
||||
def test_codexs_own_tiers_stay_only_when_every_deployment_behind_the_name_prices_them() -> None:
|
||||
stock: Final = bundled_codex_models()[_SOL_SLUG].model_dump(mode="json")
|
||||
assert stock["service_tiers"] and stock["additional_speed_tiers"] and stock["default_service_tier"]
|
||||
token_price: Final = {"input_cost_per_token": 1e-06}
|
||||
priority_price: Final = {**token_price, "input_cost_per_token_priority": 1e-05}
|
||||
|
||||
def upstream(host: str) -> dict[str, str]:
|
||||
return {"model": f"hosted_vllm/{_SOL_SLUG}", "api_base": f"https://{host}.example/v1"}
|
||||
|
||||
router: Final = litellm.Router(
|
||||
model_list=[
|
||||
{"model_name": "all-priced", "litellm_params": upstream("a"), "model_info": priority_price},
|
||||
{"model_name": "all-priced", "litellm_params": upstream("b"), "model_info": priority_price},
|
||||
{"model_name": "half-priced", "litellm_params": upstream("c"), "model_info": priority_price},
|
||||
{"model_name": "half-priced", "litellm_params": upstream("d"), "model_info": token_price},
|
||||
{"model_name": "priced-and-unknown", "litellm_params": upstream("f"), "model_info": priority_price},
|
||||
{"model_name": "priced-and-unknown", "litellm_params": {"model": "azure/prod-sol-eastus"}},
|
||||
{
|
||||
"model_name": "configured",
|
||||
"litellm_params": upstream("e"),
|
||||
"model_info": {"service_tiers": ["priority"]},
|
||||
},
|
||||
]
|
||||
)
|
||||
|
||||
served: Final = _served(router, "all-priced", "half-priced", "priced-and-unknown", "configured")
|
||||
|
||||
assert _tier_fields(served["all-priced"]) == _tier_fields(stock)
|
||||
assert _tier_fields(served["priced-and-unknown"]) == _tier_fields(stock)
|
||||
assert _tier_fields(served["half-priced"]) == ([], [], None)
|
||||
assert served["half-priced"]["model_messages"] == stock["model_messages"]
|
||||
assert [tier["id"] for tier in served["configured"]["service_tiers"]] == ["priority"]
|
||||
|
||||
|
||||
def _tiered_stock(default_service_tier: str) -> Mapping[str, CodexStockModel]:
|
||||
tiers: Final = (
|
||||
CodexServiceTier(id="priority", name="Fast", description="faster"),
|
||||
CodexServiceTier(id="flex", name="Flex", description="cheaper"),
|
||||
)
|
||||
return MappingProxyType(
|
||||
{
|
||||
"tiered": CodexStockModel(
|
||||
slug="tiered",
|
||||
display_name="Tiered",
|
||||
priority=0,
|
||||
visibility="list",
|
||||
service_tiers=tiers,
|
||||
default_service_tier=default_service_tier,
|
||||
)
|
||||
}
|
||||
)
|
||||
|
||||
|
||||
@pytest.mark.parametrize(("default_tier", "kept_default"), [("priority", "priority"), ("flex", None)])
|
||||
def test_dropping_one_stock_tier_keeps_the_default_only_while_it_is_still_offered(default_tier, kept_default) -> None:
|
||||
priority_and_flex: Final = CodexDeploymentCapabilities(
|
||||
prices_tokens=True,
|
||||
priced_service_tier_keys=frozenset(
|
||||
get_service_tier_cost_key("input_cost_per_token", tier) for tier in ("priority", "flex")
|
||||
),
|
||||
)
|
||||
priority_only: Final = CodexDeploymentCapabilities(
|
||||
prices_tokens=True,
|
||||
priced_service_tier_keys=frozenset({get_service_tier_cost_key("input_cost_per_token", "priority")}),
|
||||
)
|
||||
row: Final = _row("tiered", deployment_capabilities=(priority_and_flex, priority_only))
|
||||
|
||||
(entry,) = json.loads(
|
||||
codex_models_response_json((row,), stock=_tiered_stock(default_tier), instructions=_PROMPT).json
|
||||
)["models"]
|
||||
|
||||
assert [tier["id"] for tier in entry["service_tiers"]] == ["priority"]
|
||||
assert entry["default_service_tier"] == kept_default
|
||||
|
||||
|
||||
def test_model_info_that_does_not_validate_declares_no_capability_but_keeps_its_prices() -> None:
|
||||
priced: Final = get_service_tier_cost_key("input_cost_per_token", "priority")
|
||||
|
||||
capabilities: Final = deployment_capabilities(
|
||||
{"supports_vision": "sometimes", "supports_function_calling": False, priced: 1e-05}, "odd"
|
||||
)
|
||||
|
||||
assert capabilities == CodexDeploymentCapabilities(priced_service_tier_keys=frozenset({priced}))
|
||||
|
|
|
|||
224
tests/unit/proxy/common_utils/test_codex_model_translation.py
Normal file
224
tests/unit/proxy/common_utils/test_codex_model_translation.py
Normal file
|
|
@ -0,0 +1,224 @@
|
|||
import json
|
||||
from typing import Final
|
||||
|
||||
import pytest
|
||||
|
||||
from litellm.litellm_core_utils.llm_cost_calc.utils import get_service_tier_cost_key
|
||||
from litellm.proxy.common_utils.codex_model_translation import (
|
||||
CodexCapabilities,
|
||||
CodexDeploymentCapabilities,
|
||||
CodexServiceTier,
|
||||
codex_fallback_entry,
|
||||
combined_capabilities,
|
||||
no_priced_deployment_lacks_tier,
|
||||
)
|
||||
|
||||
_PROMPT: Final = "base prompt"
|
||||
_SILENT: Final = CodexDeploymentCapabilities()
|
||||
_ABSENT: Final = object()
|
||||
|
||||
|
||||
def _entry(capabilities: CodexCapabilities, **kwargs: object) -> dict[str, object]:
|
||||
entry: Final = codex_fallback_entry(
|
||||
slug="m", display_name="M", priority=3, capabilities=capabilities, instructions=_PROMPT, **kwargs
|
||||
)
|
||||
return json.loads(entry.model_dump_json())
|
||||
|
||||
|
||||
_CLAUDE_LIKE: Final = CodexCapabilities(
|
||||
context_window=1_000_000, supports_vision=True, supports_function_calling=True, supports_reasoning=True
|
||||
)
|
||||
|
||||
|
||||
def test_a_model_that_calls_tools_and_reasons_gets_codexs_own_coding_setup():
|
||||
entry: Final = _entry(_CLAUDE_LIKE)
|
||||
|
||||
assert entry["apply_patch_tool_type"] == "freeform"
|
||||
assert entry["truncation_policy"] == {"mode": "tokens", "limit": 10000}
|
||||
assert entry["supports_reasoning_summary_parameter"] is True
|
||||
assert entry["supports_reasoning_summaries"] is True
|
||||
assert entry["input_modalities"] == ["text", "image"]
|
||||
assert entry["context_window"] == 1_000_000
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
("flag", "field", "by_value"),
|
||||
[
|
||||
("supports_function_calling", "apply_patch_tool_type", {True: "freeform", False: None, None: None}),
|
||||
("supports_reasoning", "supports_reasoning_summary_parameter", {True: True, False: False, None: _ABSENT}),
|
||||
("supports_reasoning", "supports_reasoning_summaries", {True: True, False: False, None: False}),
|
||||
("supports_parallel_function_calling", "supports_parallel_tool_calls", {True: True, False: False, None: False}),
|
||||
],
|
||||
)
|
||||
@pytest.mark.parametrize("value", [True, False, None])
|
||||
def test_each_codex_feature_follows_what_the_deployments_declare(flag, field, by_value, value):
|
||||
"""A missing `supports_reasoning_summary_parameter` is Codex's own default (true, `default_true` in
|
||||
`codex-rs/protocol/src/openai_models.rs` at rust-v0.166.0), so a model nobody describes keeps it."""
|
||||
entry: Final = _entry(CodexCapabilities(**{flag: value}))
|
||||
|
||||
assert entry.get(field, _ABSENT) == by_value[value]
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
("vision", "modalities"), [(False, ["text"]), (True, ["text", "image"]), (None, ["text", "image"])]
|
||||
)
|
||||
def test_images_are_offered_unless_a_deployment_cannot_read_them(vision, modalities):
|
||||
assert _entry(CodexCapabilities(supports_vision=vision))["input_modalities"] == modalities
|
||||
|
||||
|
||||
def test_the_rest_of_the_entry_is_what_the_listing_decides():
|
||||
tier: Final = CodexServiceTier(id="flex", name="Flex", description="cheaper")
|
||||
|
||||
entry: Final = _entry(
|
||||
CodexCapabilities(),
|
||||
service_tiers=(tier,),
|
||||
supports_search_tool=True,
|
||||
reasoning_efforts=("low", "high"),
|
||||
client_version="0.166.0",
|
||||
)
|
||||
|
||||
assert (entry["slug"], entry["display_name"], entry["priority"], entry["base_instructions"]) == (
|
||||
"m",
|
||||
"M",
|
||||
3,
|
||||
_PROMPT,
|
||||
)
|
||||
assert entry["service_tiers"] == [{"id": "flex", "name": "Flex", "description": "cheaper"}]
|
||||
assert entry["supports_search_tool"] is True
|
||||
assert [level["effort"] for level in entry["supported_reasoning_levels"]] == ["low", "high"]
|
||||
assert entry["default_reasoning_level"] == "low"
|
||||
assert entry["context_window"] is None
|
||||
assert (entry["shell_type"], entry["visibility"], entry["supported_in_api"]) == ("unified_exec", "list", True)
|
||||
assert (entry["support_verbosity"], entry["use_responses_lite"]) == (False, False)
|
||||
assert "tool_mode" not in entry
|
||||
|
||||
|
||||
_REQUIRED_SINCE_CODEX_0_140: Final = frozenset(
|
||||
{
|
||||
"slug",
|
||||
"display_name",
|
||||
"description",
|
||||
"supported_reasoning_levels",
|
||||
"shell_type",
|
||||
"visibility",
|
||||
"supported_in_api",
|
||||
"priority",
|
||||
"availability_nux",
|
||||
"upgrade",
|
||||
"base_instructions",
|
||||
"supports_reasoning_summaries",
|
||||
"support_verbosity",
|
||||
"default_verbosity",
|
||||
"apply_patch_tool_type",
|
||||
"truncation_policy",
|
||||
"supports_parallel_tool_calls",
|
||||
"experimental_supported_tools",
|
||||
}
|
||||
)
|
||||
_CODEX_ENUMS: Final = {
|
||||
"shell_type": {"unified_exec", "disabled"},
|
||||
"visibility": {"list", "hide", "none"},
|
||||
"apply_patch_tool_type": {"freeform", None},
|
||||
}
|
||||
_CODEX_MODALITIES: Final = {"text", "image"}
|
||||
_CODEX_TRUNCATION_MODES: Final = {"bytes", "tokens"}
|
||||
|
||||
|
||||
@pytest.mark.parametrize("described", [True, False], ids=["described", "undescribed"])
|
||||
def test_every_entry_carries_the_fields_and_enum_values_every_supported_codex_decodes(described):
|
||||
"""`codex-rs/protocol/src/openai_models.rs` ModelInfo at rust-v0.140.0 and rust-v0.166.0, the fields with
|
||||
no `#[serde(default)]` and the variants of InputModality, ConfigShellToolType, ModelVisibility,
|
||||
ApplyPatchToolType and TruncationMode in both, read on 2026-11-02. Codex rejects the whole catalog when
|
||||
one entry does not decode."""
|
||||
entry: Final = _entry(
|
||||
CodexCapabilities(
|
||||
context_window=128_000,
|
||||
supports_vision=False,
|
||||
supports_function_calling=True,
|
||||
supports_parallel_function_calling=True,
|
||||
supports_reasoning=True,
|
||||
)
|
||||
if described
|
||||
else CodexCapabilities()
|
||||
)
|
||||
|
||||
assert entry.keys() >= _REQUIRED_SINCE_CODEX_0_140
|
||||
assert all(entry[field] in values for field, values in _CODEX_ENUMS.items())
|
||||
assert set(entry["input_modalities"]) <= _CODEX_MODALITIES and "text" in entry["input_modalities"]
|
||||
assert entry["truncation_policy"]["mode"] in _CODEX_TRUNCATION_MODES
|
||||
|
||||
|
||||
def test_a_deployment_that_says_nothing_does_not_vote():
|
||||
declares: Final = CodexDeploymentCapabilities(
|
||||
supports_vision=True,
|
||||
supports_function_calling=True,
|
||||
supports_parallel_function_calling=True,
|
||||
supports_reasoning=True,
|
||||
)
|
||||
|
||||
assert combined_capabilities((declares, _SILENT)) == CodexCapabilities(
|
||||
supports_vision=True,
|
||||
supports_function_calling=True,
|
||||
supports_parallel_function_calling=True,
|
||||
supports_reasoning=True,
|
||||
)
|
||||
assert combined_capabilities((_SILENT, _SILENT)) == CodexCapabilities()
|
||||
assert combined_capabilities(()) == CodexCapabilities()
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
"flag",
|
||||
["supports_vision", "supports_function_calling", "supports_parallel_function_calling", "supports_reasoning"],
|
||||
)
|
||||
def test_one_deployment_lacking_a_capability_takes_it_off_the_whole_name(flag):
|
||||
has: Final = CodexDeploymentCapabilities(**{flag: True})
|
||||
lacks: Final = CodexDeploymentCapabilities(**{flag: False})
|
||||
|
||||
for order in ((has, lacks, _SILENT), (_SILENT, lacks, has)):
|
||||
assert getattr(combined_capabilities(order), flag) is False
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
("windows", "listed", "expected"),
|
||||
[
|
||||
((200_000, 128_000, None), 400_000, 128_000),
|
||||
((None, 64_000), 400_000, 64_000),
|
||||
((None, None), 400_000, 400_000),
|
||||
((), 400_000, 400_000),
|
||||
((0, -1, 32_000), None, 32_000),
|
||||
((0,), None, None),
|
||||
],
|
||||
)
|
||||
def test_the_window_is_the_smallest_any_deployment_declares(windows, listed, expected):
|
||||
deployments: Final = tuple(CodexDeploymentCapabilities(max_input_tokens=window) for window in windows)
|
||||
|
||||
assert combined_capabilities(deployments, listed).context_window == expected
|
||||
|
||||
|
||||
def _priced(*tier_ids: str) -> CodexDeploymentCapabilities:
|
||||
return CodexDeploymentCapabilities(
|
||||
prices_tokens=True,
|
||||
priced_service_tier_keys=frozenset(
|
||||
get_service_tier_cost_key("input_cost_per_token", tier) for tier in tier_ids
|
||||
),
|
||||
)
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
("deployments", "tier", "kept"),
|
||||
[
|
||||
((_priced("priority"), _priced("priority", "flex")), "priority", True),
|
||||
((_priced("priority"), _priced("flex")), "priority", False),
|
||||
((_priced("priority"), _priced()), "priority", False),
|
||||
((_priced("priority"), _SILENT), "priority", True),
|
||||
((_SILENT, _SILENT), "priority", True),
|
||||
((_priced("priority"),), "fast", True),
|
||||
((_priced("flex"),), "fast", False),
|
||||
((_priced("priority", "flex"),), "made-up-tier", False),
|
||||
((), "priority", True),
|
||||
],
|
||||
)
|
||||
def test_a_tier_stays_unless_a_deployment_that_prices_tokens_has_no_price_for_it(deployments, tier, kept):
|
||||
"""A deployment with no token price at all (a subscription upstream, a deployment name the cost map does
|
||||
not know) says nothing about which tiers its upstream sells, so it does not vote."""
|
||||
assert no_priced_deployment_lacks_tier(tier, deployments) is kept
|
||||
|
|
@ -56,9 +56,11 @@ def patched_models(monkeypatch):
|
|||
deployment.litellm_params.model = "gpt-4"
|
||||
router.get_deployment_by_model_group_name = MagicMock(return_value=deployment)
|
||||
router.get_routable_upstream_model = MagicMock(return_value="gpt-4")
|
||||
router.get_routable_upstream_provider = MagicMock(return_value=None)
|
||||
router.get_configured_display_name = MagicMock(return_value=None)
|
||||
router.get_configured_service_tiers = MagicMock(return_value=())
|
||||
router.get_configured_max_input_tokens = MagicMock(return_value=())
|
||||
router.get_routable_deployment_model_info = MagicMock(return_value=())
|
||||
router.get_model_group_info = MagicMock(return_value=None)
|
||||
router.get_search_tool_support = MagicMock(return_value=())
|
||||
route_entry_points_to_stubs(router)
|
||||
|
|
|
|||
|
|
@ -99,6 +99,8 @@ def route_entry_points_to_stubs(router: _RouterDouble) -> _RouterDouble:
|
|||
return router.is_recognized_model(name)
|
||||
if kind == "upstream":
|
||||
return router.get_routable_upstream_model(name, **_given(team_id=team_id))
|
||||
if kind == "upstream_provider":
|
||||
return router.get_routable_upstream_provider(name, **_given(team_id=team_id))
|
||||
if kind == "search_tool_support":
|
||||
return router.get_search_tool_support(name, **_given(team_id=team_id))
|
||||
if kind == "from_id":
|
||||
|
|
@ -125,6 +127,8 @@ def route_entry_points_to_stubs(router: _RouterDouble) -> _RouterDouble:
|
|||
return router.get_configured_service_tiers(name, **_given(team_id=team_id))
|
||||
if field == "max_input_tokens":
|
||||
return router.get_configured_max_input_tokens(name, **_given(team_id=team_id))
|
||||
if field == "deployment_model_info":
|
||||
return router.get_routable_deployment_model_info(name, **_given(team_id=team_id))
|
||||
if field == "listing_info":
|
||||
return router.get_model_listing_info(name)
|
||||
if field == "mode":
|
||||
|
|
|
|||
|
|
@ -7299,6 +7299,27 @@ def test_upstream_model_and_service_tiers_are_read_off_the_deployments_the_team_
|
|||
assert router.get_configured_service_tiers(model_name, team_id) == service_tiers
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
("litellm_params", "provider"),
|
||||
[
|
||||
({"model": "us.openai.gpt-6.1-sol", "custom_llm_provider": "bedrock"}, "bedrock"),
|
||||
({"model": "bedrock_mantle/openai.gpt-6.1-sol"}, "bedrock_mantle"),
|
||||
({"model": "gpt-6.1-sol"}, None),
|
||||
],
|
||||
)
|
||||
def test_the_upstream_provider_is_the_one_the_routable_deployment_declares(litellm_params, provider):
|
||||
router = litellm.Router(
|
||||
model_list=[
|
||||
{"model_name": "sol", "litellm_params": litellm_params, "model_info": {"team_id": "team-1"}},
|
||||
{"model_name": "sol", "litellm_params": {"model": "azure/sol-deployment"}, "model_info": {"team_id": "t2"}},
|
||||
]
|
||||
)
|
||||
|
||||
assert router.registry().resolve("sol", kind="upstream_provider", team_id="team-1") == provider
|
||||
assert router.registry().resolve("sol", kind="upstream_provider", team_id="t2") == "azure"
|
||||
assert router.registry().resolve("not-a-real-model", kind="upstream_provider") is None
|
||||
|
||||
|
||||
def test_get_configured_display_name_treats_malformed_values_as_absent():
|
||||
malformed = ["", " ", 12345, ["Kimi K3"], {"name": "Kimi K3"}, True]
|
||||
router = litellm.Router(
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue