fix(cli): give lite codex aliases of a Codex-known model that model's catalog entry (#48894)

* fix(cli): give lite codex aliases of a Codex-known model that model's catalog entry

lite codex built its Codex model catalog from /v1/models ids only, so a proxy
alias of a model Codex knows (api-gpt-6-luna for openai/gpt-6-luna) got the
fallback entry. That entry has no supports_search_tool or tool_mode, so Codex
sent every MCP and app tool inline and each turn cost far more input tokens.

lite now reads each listed model's upstream from the proxy's /v1/model/info
(team_public_model_name when set, else model_name, first deployment wins) and
passes it to the shared catalog builder, the same way the proxy's own Codex
route does. If that request fails, the catalog is built as before.

* fix(cli): give a lite codex alias an upstream only when all its deployments route to one OpenAI model

* fix(cli): cap a lite codex alias's context window at its listed max_input_tokens and count only OpenAI-hosted deployments

* fix(cli): keep Codex's tier, upgrade and reasoning defaults off lite codex aliases

* fix(cli): keep Codex's own effort, verbosity and promo off lite codex aliases and skip credential-hosted deployments

---------

Co-authored-by: mateo-berri <277851410+mateo-berri@users.noreply.github.com>
This commit is contained in:
tessellark 2026-10-28 21:23:55 -07:00 • committed by GitHub
parent dbaae521f9
commit 5bd47442fb
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
4 changed files with 483 additions and 19 deletions

View file

@ -5,16 +5,18 @@ import shutil
import subprocess
import sys
import tempfile
from collections.abc import Callable, Mapping, Sequence
from collections import Counter
from collections.abc import Callable, Hashable, Iterable, Mapping, Sequence
from dataclasses import dataclass
from itertools import chain
from pathlib import Path
from types import MappingProxyType
from typing import Final, TypeAlias, TypeVar
from urllib.parse import urlsplit
import click
import requests
from pydantic import JsonValue, TypeAdapter, ValidationError
from pydantic import ConfigDict, JsonValue, TypeAdapter, ValidationError
from litellm.proxy.common_utils.codex_model_catalog import (
CODEX_BASE_INSTRUCTIONS_PATH,
@ -497,6 +499,113 @@ def _fetch_model_listing(
return listing.data
class _DeploymentParams(LiteLLMBaseModel):
model_config = ConfigDict(frozen=True)
model: str
custom_llm_provider: str | None = None
api_base: str | None = None
litellm_credential_name: str | None = None
reasoning_effort: JsonValue = None
class _DeploymentInfo(LiteLLMBaseModel):
model_config = ConfigDict(frozen=True)
team_public_model_name: str | None = None
blocked: bool | None = None
class _Deployment(LiteLLMBaseModel):
model_config = ConfigDict(frozen=True)
model_name: str
litellm_params: _DeploymentParams
model_info: _DeploymentInfo | None = None
class _DeploymentListing(LiteLLMBaseModel):
model_config = ConfigDict(frozen=True)
data: tuple[_Deployment, ...]
@dataclass(frozen=True, slots=True)
class ModelUpstream:
"""The OpenAI model every deployment behind a public name routes to, and the reasoning effort they all pin."""
model: str
reasoning_effort: str | None = None
_NO_UPSTREAMS: Final[Mapping[str, ModelUpstream]] = MappingProxyType({})
_Pinned: Final = TypeVar("_Pinned", bound=Hashable)
def _public_name(deployment: _Deployment) -> str:
team_name: Final = deployment.model_info.team_public_model_name if deployment.model_info is not None else None
return team_name or deployment.model_name
def _is_paused(deployment: _Deployment) -> bool:
return deployment.model_info is not None and deployment.model_info.blocked is True
def _is_openai_host(params: _DeploymentParams) -> bool:
if params.api_base is None:
return not params.litellm_credential_name
host: Final = urlsplit(params.api_base).hostname or ""
return host == "api.openai.com" or host.endswith(".api.openai.com")
def _openai_model(params: _DeploymentParams) -> str | None:
if params.custom_llm_provider not in (None, "openai") or not _is_openai_host(params):
return None
provider, slash, name = params.model.partition("/")
if not slash:
return params.model
return name if provider == "openai" else None
def _pinned_effort(params: _DeploymentParams) -> str | None:
return params.reasoning_effort if isinstance(params.reasoning_effort, str) else None
def _shared_by_every_deployment(values: Iterable[tuple[str, _Pinned]]) -> Mapping[str, _Pinned]:
distinct: Final = frozenset(values)
values_per_name: Final = Counter(name for name, _ in distinct)
return MappingProxyType({name: value for name, value in distinct if values_per_name[name] == 1})
def _upstreams_by_public_name(deployments: Sequence[_Deployment]) -> Mapping[str, ModelUpstream]:
live: Final = tuple(d for d in deployments if not _is_paused(d))
models: Final = _shared_by_every_deployment((_public_name(d), _openai_model(d.litellm_params)) for d in live)
efforts: Final = _shared_by_every_deployment((_public_name(d), _pinned_effort(d.litellm_params)) for d in live)
return MappingProxyType(
{name: ModelUpstream(model, efforts.get(name)) for name, model in models.items() if model is not None}
)
def _fetch_model_upstreams(
base_url: str,
api_key: str,
*,
get: Callable[..., requests.Response],
) -> Mapping[str, ModelUpstream]:
url: Final = base_url.rstrip("/") + "/v1/model/info"
try:
resp: Final = get(url, headers=MappingProxyType({"Authorization": f"Bearer {api_key}"}), timeout=10)
except requests.RequestException:
return _NO_UPSTREAMS
if resp.status_code != 200:
return _NO_UPSTREAMS
try:
listing: Final = _DeploymentListing.model_validate_json(resp.content)
except ValidationError:
return _NO_UPSTREAMS
return _upstreams_by_public_name(listing.data)
class _OpenCodeLimit(LiteLLMBaseModel):
context: int
output: int
@ -576,8 +685,21 @@ def opencode_model_sync_env(
return MappingProxyType({OPENCODE_CONFIG_CONTENT_ENV: opencode_provider_config(base_url, listing)})
def _codex_row(model: ListedModel, upstream: ModelUpstream | None) -> CodexCatalogRow:
return CodexCatalogRow(
id=model.id,
mode=model.mode,
max_input_tokens=model.max_input_tokens,
upstream_model=upstream.model if upstream is not None else None,
reasoning_effort=upstream.reasoning_effort if upstream is not None else None,
)
def codex_model_catalog(
models: Sequence[ListedModel], stock: Sequence[CodexStockModel], instructions: str
models: Sequence[ListedModel],
stock: Sequence[CodexStockModel],
instructions: str,
upstreams: Mapping[str, ModelUpstream],
) -> str | None:
"""The `model_catalog_json` body listing the proxy's chat models, or None if there are none.
@ -586,14 +708,23 @@ def codex_model_catalog(
installed Codex knows keeps that Codex's own entry and the proxy only
decides its place in the picker: the listing orders it, lists it even when
Codex hides it or keeps it off the API, and keeps Codex's upgrade nudge only
when the model it points at is served too. A model Codex does not know gets the fallback
when the model it points at is served too. An alias of such a model, per
`upstreams`, takes that entry too, minus the defaults Codex picks for the
model: no service tier or upgrade nudge, the reasoning effort its
deployments pin, and a window no larger than its listed max_input_tokens.
A model Codex does not know gets the fallback
entry, with the same base instructions Codex itself uses so the agent never
runs without a system prompt. The catalog is the one `/v1/models?client_version=...`
serves, built from the installed Codex's own entries instead of the proxy's vendored ones.
runs without a system prompt. Apart from aliases, the catalog is the one
`/v1/models?client_version=...` serves, built from the installed Codex's own
entries instead of the proxy's vendored ones.
"""
rows: Final = tuple(CodexCatalogRow(id=m.id, mode=m.mode, max_input_tokens=m.max_input_tokens) for m in models)
rows: Final = tuple(_codex_row(m, upstreams.get(m.id)) for m in models)
body: Final = codex_models_response_json(
rows, stock=MappingProxyType({m.slug: m for m in stock}), instructions=instructions, byte_limit=None
rows,
stock=MappingProxyType({m.slug: m for m in stock}),
instructions=instructions,
byte_limit=None,
alias_defaults="deployment",
)
return body.json if body.listed else None
@ -678,12 +809,14 @@ def codex_model_sync_args(
Codex has no env or inline equivalent of OPENCODE_CONFIG_CONTENT: the catalog
must be a file, so it is written under $CODEX_HOME (default ~/.codex) and
atomically replaced on every launch. The Codex at `binary` first lists its
own models, so the ones the proxy serves keep that Codex's entries, and then
own models, so the ones the proxy serves keep that Codex's entries, an alias
included when every unpaused deployment /v1/model/info lists for it routes
to the same OpenAI model and Codex knows that model, and then
reads the file back once before it is handed over. The key never lands in
the file. A failed fetch, read, listing, write or read-back is reported
rather than raised: Codex still launches with its built-in catalog and takes
a proxy model by name via -m, and a rejected file stays on disk to be looked
at.
at. An unavailable /v1/model/info only leaves every alias on the fallback entry.
"""
listing: Final = _fetch_model_listing(base_url, api_key, get=get)
if isinstance(listing, ModelSyncSkipped):
@ -700,7 +833,8 @@ def codex_model_sync_args(
stock: Final = _stock_codex_models(binary, base_env, run=run)
if isinstance(stock, ModelSyncSkipped):
return stock
catalog: Final = codex_model_catalog(listing, stock, instructions)
upstreams: Final = _fetch_model_upstreams(base_url, api_key, get=get)
catalog: Final = codex_model_catalog(listing, stock, instructions, upstreams)
if catalog is None:
return ModelSyncSkipped(f"{base_url.rstrip('/')}/v1/models lists no chat models")
try:

View file

@ -24,7 +24,7 @@ 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
from typing import TYPE_CHECKING, Annotated, Final, Literal, TypeAlias
from pydantic import ConfigDict, StringConstraints, TypeAdapter, ValidationError
@ -110,6 +110,12 @@ class CodexStockUpgrade(LiteLLMBaseModel):
model: str
class CodexReasoningLevel(LiteLLMBaseModel):
model_config = ConfigDict(frozen=True, extra="allow")
effort: str
class CodexStockModel(LiteLLMBaseModel):
"""One `ModelInfo` entry as Codex ships it or prints it from `codex debug models`.
@ -128,6 +134,8 @@ class CodexStockModel(LiteLLMBaseModel):
upgrade: CodexStockUpgrade | None = None
service_tiers: tuple[CodexServiceTier, ...] = ()
default_service_tier: str | None = None
supported_reasoning_levels: tuple[CodexReasoningLevel, ...] = ()
context_window: int | None = None
class CodexStockCatalog(LiteLLMBaseModel):
@ -135,19 +143,21 @@ class CodexStockCatalog(LiteLLMBaseModel):
CodexCatalogEntry = CodexFallbackModel | CodexStockModel
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, 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."""
input limit, the upstream model behind it, 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."""
id: str
mode: str | None = None
max_input_tokens: int | None = None
upstream_model: str | None = None
reasoning_effort: str | None = None
display_name: str | None = None
service_tiers: tuple[object, ...] = ()
@ -254,12 +264,42 @@ def _stock_for(row: CodexCatalogRow, stock: Mapping[str, CodexStockModel]) -> Co
return upstream if upstream is not None else stock.get(row.upstream_model.partition("/")[2])
def _alias_context_window(row: CodexCatalogRow, stock_model: CodexStockModel) -> Mapping[str, int]:
if row.max_input_tokens is None or stock_model.context_window is None:
return MappingProxyType({})
return MappingProxyType({"context_window": min(stock_model.context_window, row.max_input_tokens)})
def _alias_reasoning_level(row: CodexCatalogRow, stock_model: CodexStockModel) -> str | None:
efforts: Final = frozenset(level.effort for level in stock_model.supported_reasoning_levels)
return row.reasoning_effort if row.reasoning_effort in efforts else None
def _deployment_defaults(row: CodexCatalogRow, stock_model: CodexStockModel) -> Mapping[str, object]:
"""What an alias's deployments decide in place of the defaults Codex picks for the model they route to:
no service tier, verbosity, upgrade nudge or startup promo of Codex's own, so what the deployment
pins (or OpenAI's API default) stays in force, the pinned reasoning effort when Codex offers it and
none otherwise, and a window no larger than the alias's own limit."""
return MappingProxyType(
{
"service_tiers": (),
"default_service_tier": None,
"default_verbosity": None,
"upgrade": None,
"availability_nux": None,
"default_reasoning_level": _alias_reasoning_level(row, stock_model),
**_alias_context_window(row, stock_model),
}
)
def _entry(
index: int,
row: CodexCatalogRow,
stock: Mapping[str, CodexStockModel],
served: frozenset[str],
instructions: str,
alias_defaults: CodexAliasDefaults,
) -> CodexCatalogEntry:
stock_model: Final = _stock_for(row, stock)
stock_tiers: Final = MappingProxyType(
@ -278,7 +318,8 @@ def _entry(
upgrade: Final = (
stock_model.upgrade if stock_model.upgrade is not None and stock_model.upgrade.model in served else None
)
stock_name: Final = stock_model.display_name if row.id == stock_model.slug else row.id
is_alias: Final = row.id != stock_model.slug
stock_name: Final = row.id if is_alias else stock_model.display_name
listing: Final = {
"slug": row.id,
"display_name": row.display_name if row.display_name is not None else stock_name,
@ -286,6 +327,7 @@ def _entry(
"visibility": "list",
"supported_in_api": True,
"upgrade": upgrade,
**(_deployment_defaults(row, stock_model) if is_alias and alias_defaults == "deployment" else {}),
}
if configured is None:
return stock_model.model_copy(update=listing)
@ -351,6 +393,7 @@ def codex_models_response_json(
stock: Mapping[str, CodexStockModel] | None = None,
instructions: str | None = None,
byte_limit: int | None = CODEX_CATALOG_BYTE_LIMIT,
alias_defaults: CodexAliasDefaults = "codex",
) -> CodexCatalogBody:
"""Codex's `ModelsResponse` body for `rows`, in listing order, cut to Codex's byte limit.
@ -359,13 +402,18 @@ def codex_models_response_json(
byte limit silently and keeps its bundled one, so the body holds only the entries that fit,
the ones offering a service tier taken first and the rest in listing order, an entry too large
for the bytes left passed over, every kept entry at its listing position; `byte_limit=None`
keeps every entry.
keeps every entry. 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.
"""
known: Final = bundled_codex_models() if stock is None else stock
prompt: Final = codex_base_instructions() if instructions is None else instructions
drivable: Final = tuple(row for row in rows if _codex_can_drive(row))
served: Final = frozenset(row.id for row in drivable)
entries: Final = tuple(_entry(index, row, known, served, prompt) for index, row in enumerate(drivable))
entries: Final = tuple(
_entry(index, row, known, served, prompt, alias_defaults) for index, row in enumerate(drivable)
)
serialized: Final = tuple(_serialized(entry) for entry in entries)
kept: Final = _kept_positions(entries, serialized, byte_limit)
return CodexCatalogBody(

View file

@ -92,6 +92,8 @@ _STOCK_MODELS = {
"base_instructions": "You are Codex, a coding agent based on GPT-5.6.",
"apply_patch_tool_type": "freeform",
"supports_parallel_tool_calls": True,
"supports_search_tool": True,
"tool_mode": "code_mode_only",
"context_window": 272000,
"comp_hash": "terra-hash",
},
@ -669,10 +671,16 @@ class TestCodexModelSync:
def _row(model_id, **extra):
return {"id": model_id, "object": "model", "created": 1, "owned_by": "openai", **extra}
def _sync(self, listing, codex_home, base_url="http://localhost:4000/", run=None):
def _sync(self, listing, codex_home, base_url="http://localhost:4000/", run=None, model_info=None):
captured = {}
def fake_get(url, headers, timeout):
if headers.get("Authorization") != "Bearer sk-key":
return _FakeResponse(401)
if url.endswith("/v1/model/info"):
if isinstance(model_info, Exception):
raise model_info
return model_info if model_info is not None else _FakeResponse(200, {"data": []})
captured["url"] = url
captured["headers"] = headers
return _FakeResponse(200, listing)
@ -737,6 +745,202 @@ class TestCodexModelSync:
assert entries["gpt-5.4"]["upgrade"] == _STOCK_MODELS["gpt-5.4"]["upgrade"]
assert [entries["gpt-5.4"]["priority"], entries["gpt-5.6-terra"]["priority"]] == [0, 1]
@pytest.mark.parametrize(
"deployment",
[
{"model_name": "api-terra", "litellm_params": {"model": "openai/gpt-5.6-terra"}},
{"model_name": "api-terra", "litellm_params": {"model": "gpt-5.6-terra"}, "model_info": None},
{
"model_name": "api-terra",
"litellm_params": {"model": "openai/gpt-5.6-terra"},
"model_info": {"id": "d1", "team_public_model_name": ""},
},
{
"model_name": "model_name_team-1_d1",
"litellm_params": {"model": "openai/gpt-5.6-terra", "api_base": "https://api.openai.com/v1"},
"model_info": {"id": "d1", "team_public_model_name": "api-terra"},
},
],
ids=["provider_prefixed", "bare_without_model_info", "empty_team_public_name", "team_public_model_name"],
)
def test_alias_of_a_codex_model_gets_that_models_entry_under_its_own_name(self, tmp_path, deployment):
listing = self._listing(self._row("api-terra"), self._row("m"))
_, result = self._sync(listing, tmp_path, model_info=_FakeResponse(200, {"data": [deployment]}))
assert isinstance(result, ModelSyncArgs)
entries = self._entries(tmp_path)
assert entries["api-terra"] == {
**_STOCK_MODELS["gpt-5.6-terra"],
"slug": "api-terra",
"display_name": "api-terra",
"priority": 0,
"service_tiers": [],
"default_service_tier": None,
"default_verbosity": None,
"availability_nux": None,
"default_reasoning_level": None,
}
assert "supports_search_tool" not in entries["m"]
def test_alias_gets_no_upgrade_nudge_away_from_its_own_name(self, tmp_path):
listing = self._listing(self._row("gpt-5.6-terra"), self._row("api-54"))
deployment = {"model_name": "api-54", "litellm_params": {"model": "openai/gpt-5.4"}}
self._sync(listing, tmp_path, model_info=_FakeResponse(200, {"data": [deployment]}))
entry = self._entries(tmp_path)["api-54"]
assert (entry["comp_hash"], entry["upgrade"]) == (_STOCK_MODELS["gpt-5.4"]["comp_hash"], None)
@pytest.mark.parametrize(
("pins", "expected"),
[
(("high",), "high"),
(("high", "high"), "high"),
(("high", None), None),
(("high", "low"), None),
(({"effort": "high"},), None),
],
ids=["one_deployment", "every_deployment_agrees", "one_unpinned", "pins_differ", "non_string_pin"],
)
def test_alias_starts_at_the_reasoning_effort_every_deployment_pins(self, tmp_path, pins, expected):
deployments = [
{
"model_name": "api-terra",
"litellm_params": {"model": "openai/gpt-5.6-terra", **({"reasoning_effort": p} if p else {})},
}
for p in pins
]
self._sync(
self._listing(self._row("api-terra")), tmp_path, model_info=_FakeResponse(200, {"data": deployments})
)
entry = self._entries(tmp_path)["api-terra"]
assert (entry["comp_hash"], entry["default_reasoning_level"]) == ("terra-hash", expected)
@pytest.mark.parametrize(
"deployments",
[
[
{"model_name": "api-terra", "litellm_params": {"model": "openai/gpt-5.6-terra"}},
{"model_name": "api-terra", "litellm_params": {"model": "gpt-5.6-terra"}},
],
[
{
"model_name": "api-terra",
"litellm_params": {"model": "gpt-5.6-terra", "custom_llm_provider": "openai"},
}
],
[
{
"model_name": "api-terra",
"litellm_params": {"model": "openai/gpt-5.5"},
"model_info": {"id": "paused", "blocked": True},
},
{"model_name": "api-terra", "litellm_params": {"model": "openai/gpt-5.6-terra"}},
],
[
{
"model_name": "api-terra",
"litellm_params": {"model": "openai/gpt-5.6-terra", "api_base": "https://eu.api.openai.com/v1"},
}
],
[
{
"model_name": "api-terra",
"litellm_params": {
"model": "openai/gpt-5.6-terra",
"api_base": "https://api.openai.com/v1",
"litellm_credential_name": "openai-key",
},
}
],
],
ids=[
"same_model_with_and_without_prefix",
"openai_custom_llm_provider",
"paused_deployment_ignored",
"openai_regional_host",
"credential_under_an_openai_host",
],
)
def test_alias_whose_deployments_all_route_to_one_openai_model_gets_its_entry(self, tmp_path, deployments):
self._sync(
self._listing(self._row("api-terra")), tmp_path, model_info=_FakeResponse(200, {"data": deployments})
)
assert self._entries(tmp_path)["api-terra"]["comp_hash"] == _STOCK_MODELS["gpt-5.6-terra"]["comp_hash"]
@pytest.mark.parametrize(
"deployments",
[
[
{"model_name": "api-terra", "litellm_params": {"model": "openai/gpt-5.5"}},
{
"model_name": "model_name_team-1_d1",
"litellm_params": {"model": "openai/gpt-5.6-terra"},
"model_info": {"id": "d1", "team_public_model_name": "api-terra"},
},
],
[
{"model_name": "api-terra", "litellm_params": {"model": "openai/gpt-5.6-terra"}},
{"model_name": "api-terra", "litellm_params": {"model": "anthropic/claude-opus-4-7"}},
],
[{"model_name": "api-terra", "litellm_params": {"model": "azure/gpt-5.6-terra"}}],
[{"model_name": "api-terra", "litellm_params": {"model": "gpt-5.6-terra", "custom_llm_provider": "azure"}}],
[
{
"model_name": "api-terra",
"litellm_params": {"model": "openai/gpt-5.6-terra", "api_base": "http://vllm.example.invalid/v1"},
}
],
[
{
"model_name": "api-terra",
"litellm_params": {"model": "gpt-5.6-terra", "api_base": "https://api.openai.com.example.invalid"},
}
],
[
{
"model_name": "api-terra",
"litellm_params": {"model": "openai/gpt-5.6-terra", "litellm_credential_name": "gateway"},
}
],
],
ids=[
"team_row_and_global_row_differ",
"load_balanced_across_providers",
"azure_prefix",
"azure_provider",
"openai_prefix_on_another_host",
"openai_lookalike_host",
"host_only_a_credential_knows",
],
)
def test_alias_without_one_shared_openai_upstream_keeps_the_fallback_entry(self, tmp_path, deployments):
listing = self._listing(self._row("api-terra"))
self._sync(listing, tmp_path / "with", model_info=_FakeResponse(200, {"data": deployments}))
self._sync(listing, tmp_path / "without")
assert self._entries(tmp_path / "with")["api-terra"] == self._entries(tmp_path / "without")["api-terra"]
@pytest.mark.parametrize(
"model_info",
[
requests.ConnectionError("refused"),
_FakeResponse(500, {"data": [{"model_name": "api-terra", "litellm_params": {"model": "gpt-5.6-terra"}}]}),
_FakeResponse(200, {"data": "nope"}),
_FakeResponse(200, {"data": [{"model_name": "api-terra"}]}),
],
ids=["unreachable", "http_500", "unexpected_body", "deployment_without_litellm_params"],
)
def test_unavailable_model_info_keeps_the_alias_on_the_fallback_entry(self, tmp_path, model_info):
_, result = self._sync(self._listing(self._row("api-terra")), tmp_path, model_info=model_info)
catalog = tmp_path / "litellm-models.json"
assert result == ModelSyncArgs(("-c", f"model_catalog_json={json.dumps(str(catalog))}"))
entry = self._entries(tmp_path)["api-terra"]
assert entry["slug"] == "api-terra"
assert "supports_search_tool" not in entry
def test_stock_catalog_is_decoded_as_utf8_regardless_of_locale(self, tmp_path):
description = "Modelo equilibrado para el trabajo diario, con acentos y ñ."
catalog = {"models": [{**_STOCK_MODELS["gpt-5.5"], "description": description}]}

View file

@ -135,6 +135,84 @@ def test_alias_of_a_known_upstream_model_takes_stock_metadata_under_its_own_name
assert (entry["slug"], entry["display_name"]) == ("team-55", "team-55")
@pytest.mark.parametrize(("max_input_tokens", "expected"), [(100000, 100000), (922000, 272000), (None, 272000)])
def test_alias_with_deployment_defaults_caps_codexs_window_at_its_listed_max_input_tokens(max_input_tokens, expected):
stock = MappingProxyType({"gpt-5.5": _stock("gpt-5.5").model_copy(update={"context_window": 272000})})
alias = _row("team-55", upstream_model="openai/gpt-5.5", max_input_tokens=max_input_tokens)
body = codex_models_response_json((alias,), stock=stock, instructions=_PROMPT, alias_defaults="deployment")
assert json.loads(body.json)["models"][0]["context_window"] == expected
def test_alias_with_deployment_defaults_drops_codexs_tier_and_upgrade_while_the_native_name_keeps_them():
rows = (_row("gpt-6-sol"), _row("team-sol", upstream_model="gpt-6-sol"), _row("team-55", upstream_model="gpt-5.5"))
codex = {entry["slug"]: entry for entry in _models(*rows)}
deployment = {entry["slug"]: entry for entry in _models(*rows, alias_defaults="deployment")}
assert deployment["gpt-6-sol"] == codex["gpt-6-sol"]
assert (codex["team-sol"]["default_service_tier"], codex["team-55"]["upgrade"]) == (
"priority",
{"model": "gpt-6-sol"},
)
assert _tiers(codex["team-sol"]) == _tiers(codex["gpt-6-sol"]) != []
assert [
(deployment[slug]["service_tiers"], deployment[slug]["default_service_tier"])
for slug in ("team-sol", "team-55")
] == [
([], None),
([], None),
]
assert deployment["team-55"]["upgrade"] is None
assert deployment["team-sol"]["model_messages"] == codex["team-sol"]["model_messages"]
@pytest.mark.parametrize(("pinned", "expected"), [("high", "high"), ("turbo", None), (None, None)])
def test_alias_with_deployment_defaults_starts_only_at_a_reasoning_effort_its_deployments_pin(pinned, expected):
stock = CodexStockModel.model_validate(
{
"slug": "gpt-5.5",
"display_name": "GPT-5.5",
"priority": 1,
"visibility": "list",
"default_reasoning_level": "medium",
"supported_reasoning_levels": [{"effort": e, "description": e} for e in ("low", "medium", "high")],
}
)
alias = _row("team-55", upstream_model="gpt-5.5", reasoning_effort=pinned)
body = codex_models_response_json(
(alias,), stock=MappingProxyType({"gpt-5.5": stock}), instructions=_PROMPT, alias_defaults="deployment"
)
assert json.loads(body.json)["models"][0]["default_reasoning_level"] == expected
def test_alias_with_deployment_defaults_sends_no_verbosity_or_promo_of_codexs_while_the_native_name_keeps_them():
stock = CodexStockModel.model_validate(
{
"slug": "gpt-6-sol",
"display_name": "GPT-6 Sol",
"priority": 1,
"visibility": "list",
"support_verbosity": True,
"default_verbosity": "low",
"availability_nux": {"message": "GPT-6 Sol is here"},
}
)
rows = (_row("gpt-6-sol"), _row("team-sol", upstream_model="gpt-6-sol"))
body = codex_models_response_json(
rows, stock=MappingProxyType({"gpt-6-sol": stock}), instructions=_PROMPT, alias_defaults="deployment"
)
native, alias = json.loads(body.json)["models"]
assert [(e["support_verbosity"], e["default_verbosity"], e["availability_nux"]) for e in (native, alias)] == [
(True, "low", {"message": "GPT-6 Sol is here"}),
(True, None, None),
]
@pytest.mark.parametrize(
("row", "expected"),
[