feat(proxy): ask OpenAI for the Codex catalog before GitHub, keyed by the deployment's OpenAI key

Co-Authored-By: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>
This commit is contained in:
mateo 2026-11-03 22:09:46 +00:00
parent edf483ef3c
commit f1b51ee9f3
6 changed files with 234 additions and 22 deletions

View file

@ -18,7 +18,7 @@ import asyncio
import os
import re
import time
from collections import OrderedDict
from collections import Counter, OrderedDict
from collections.abc import Awaitable, Callable
from dataclasses import dataclass
from functools import cache
@ -38,6 +38,7 @@ from litellm.proxy.common_utils.codex_model_catalog import (
codex_base_instructions,
)
CODEX_CATALOG_OPENAI_URL: Final = "https://chatgpt.com/backend-api/codex/models"
CODEX_CATALOG_BASE_URL: Final = "https://raw.githubusercontent.com/openai/codex"
CODEX_CATALOG_RETRY_SECONDS: Final = 300.0
CODEX_CATALOG_FETCH_TIMEOUT_SECONDS: Final = 10.0
@ -49,7 +50,13 @@ _PROMPT_PATH: Final = "codex-rs/models-manager/prompt.md"
class _AsyncGetClient(Protocol):
def get(self, url: str, *, timeout: float | None = None) -> Awaitable[httpx.Response]: ...
def get(
self,
url: str,
*,
headers: dict[str, str] | None = None, # mutable-ok: httpx's own get signature
timeout: float | None = None,
) -> Awaitable[httpx.Response]: ...
@dataclass(frozen=True, slots=True)
@ -103,6 +110,7 @@ class CodexCatalogSource:
*,
client: _AsyncGetClient | None = None,
base_url: str | None = None,
openai_url: str | None = None,
clock: Callable[[], float] = time.monotonic,
retry_seconds: float = CODEX_CATALOG_RETRY_SECONDS,
timeout: float = CODEX_CATALOG_FETCH_TIMEOUT_SECONDS,
@ -112,6 +120,11 @@ class CodexCatalogSource:
self._base_url = (
base_url if base_url is not None else os.getenv("LITELLM_CODEX_CATALOG_BASE_URL", CODEX_CATALOG_BASE_URL)
).rstrip("/")
self._openai_url = (
openai_url
if openai_url is not None
else os.getenv("LITELLM_CODEX_CATALOG_OPENAI_URL", CODEX_CATALOG_OPENAI_URL)
)
self._clock = clock
self._retry_seconds = retry_seconds
self._timeout = timeout
@ -119,9 +132,11 @@ class CodexCatalogSource:
self._cache: OrderedDict[str, _Cached] = OrderedDict() # mutable-ok: filled as releases are first asked for
self._pending: dict[str, asyncio.Task[CodexStock]] = {} # mutable-ok: the fetches in flight, one per release
async def stock(self, client_version: str | None) -> CodexStock:
async def stock(self, client_version: str | None, *, openai_api_key: str | None = None) -> CodexStock:
"""The catalog for ``client_version``: OpenAI's live one when an OpenAI key is at hand,
else the release's models.json on GitHub, else the bundled pair."""
tag: Final = codex_release_tag(client_version)
if tag is None or uses_vendored_codex_catalog():
if tag is None or client_version is None or uses_vendored_codex_catalog():
return vendored_codex_stock()
cached: Final = self._cache.get(tag)
if cached is not None and (cached.retry_at is None or cached.retry_at > self._clock()):
@ -130,13 +145,13 @@ class CodexCatalogSource:
pending: Final = self._pending.get(tag)
if pending is not None:
return await asyncio.shield(pending)
task: Final = asyncio.ensure_future(self._resolve(tag))
task: Final = asyncio.ensure_future(self._resolve(tag, client_version, openai_api_key))
self._pending[tag] = task
task.add_done_callback(lambda _: self._pending.pop(tag, None))
return await asyncio.shield(task)
async def _resolve(self, tag: str) -> CodexStock:
fetched: Final = await self._fetch(tag)
async def _resolve(self, tag: str, version: str, openai_api_key: str | None) -> CodexStock:
fetched: Final = await self._fetch(tag, version, openai_api_key)
if isinstance(fetched, _FetchFailed):
verbose_proxy_logger.warning(
"Codex catalog %s not fetched (%s), serving the %s one for the next %.0fs",
@ -146,7 +161,9 @@ class CodexCatalogSource:
self._retry_seconds,
)
else:
verbose_proxy_logger.info("Codex catalog %s fetched, %d models", tag, len(fetched.models))
verbose_proxy_logger.info(
"Codex catalog %s fetched from %s, %d models", tag, fetched.source, len(fetched.models)
)
entry: Final = (
_Cached(stock=vendored_codex_stock(), retry_at=self._clock() + self._retry_seconds)
if isinstance(fetched, _FetchFailed)
@ -158,7 +175,35 @@ class CodexCatalogSource:
self._cache.popitem(last=False)
return entry.stock
async def _fetch(self, tag: str) -> CodexStock | _FetchFailed:
async def _fetch(self, tag: str, version: str, openai_api_key: str | None) -> CodexStock | _FetchFailed:
if openai_api_key:
fetched: Final = await self._fetch_openai(version, openai_api_key)
if not isinstance(fetched, _FetchFailed):
return fetched
verbose_proxy_logger.info(
"Codex catalog %s not fetched from OpenAI (%s), trying GitHub", tag, fetched.reason
)
return await self._fetch_github(tag)
async def _fetch_openai(self, version: str, openai_api_key: str) -> CodexStock | _FetchFailed:
url: Final = f"{self._openai_url}?client_version={version}"
body: Final = await self._get(url, headers={"Authorization": f"Bearer {openai_api_key}"})
if isinstance(body, _FetchFailed):
return body
try:
catalog: Final = CodexStockCatalog.model_validate_json(body)
except ValidationError as error:
return _FetchFailed(reason=f"{url} body does not parse: {type(error).__name__}")
prompts: Final = Counter(model.base_instructions for model in catalog.models if model.base_instructions.strip())
if not catalog.models or not prompts:
return _FetchFailed(reason=f"{url} catalog is empty")
return CodexStock(
models=MappingProxyType({model.slug: model for model in catalog.models}),
instructions=prompts.most_common(1)[0][0],
source=f"openai {version}",
)
async def _fetch_github(self, tag: str) -> CodexStock | _FetchFailed:
models, prompt = await asyncio.gather(
self._get(f"{self._base_url}/{tag}/{_MODELS_PATH}"), self._get(f"{self._base_url}/{tag}/{_PROMPT_PATH}")
)
@ -176,13 +221,17 @@ class CodexCatalogSource:
return CodexStock(
models=MappingProxyType({model.slug: model for model in catalog.models}),
instructions=instructions,
source=tag,
source=f"github {tag}",
)
async def _get(self, url: str) -> bytes | _FetchFailed:
async def _get(
self,
url: str,
headers: dict[str, str] | None = None, # mutable-ok: httpx's own get signature
) -> bytes | _FetchFailed:
client: Final = self._client if self._client is not None else _default_client()
try:
response: Final = await client.get(url, timeout=self._timeout)
response: Final = await client.get(url, headers=headers, timeout=self._timeout)
except httpx.HTTPError as error:
return _FetchFailed(reason=f"{type(error).__name__} fetching {url}")
if response.status_code != 200:

View file

@ -23,6 +23,7 @@ newer tag and renaming the json after it.
from __future__ import annotations
import os
from collections.abc import Mapping, Sequence
from dataclasses import dataclass
from functools import cache
@ -36,12 +37,14 @@ from pydantic import ConfigDict, Field, StringConstraints, TypeAdapter, Validati
from litellm._logging import verbose_proxy_logger
from litellm.litellm_core_utils.reasoning_effort_strength import nearest_declared_reasoning_effort
from litellm.secret_managers.main import get_secret_str
from litellm.types.llms.base_model import LiteLLMBaseModel
if TYPE_CHECKING:
from litellm.router import Router
from litellm.router_api import RouterRegistry
from litellm.types.proxy.model_listing import ModelInfoResponse
from litellm.types.router import DeploymentTypedDict
CODEX_BASE_INSTRUCTIONS_PATH: Final = Path(__file__).with_name("codex_base_instructions.md")
CODEX_BUNDLED_VERSION: Final = "0.166.0"
@ -164,6 +167,7 @@ class CodexStockModel(LiteLLMBaseModel):
supported_reasoning_levels: tuple[CodexReasoningLevel, ...] = ()
context_window: int | None = None
max_context_window: int | None = None
base_instructions: str = ""
class CodexStockCatalog(LiteLLMBaseModel):
@ -565,6 +569,38 @@ def _catalog_row(
)
def _deployment_openai_key(deployment: DeploymentTypedDict) -> str | None:
params: Final = MappingProxyType[str, object](deployment["litellm_params"])
model: Final = params.get("model")
provider: Final = params.get("custom_llm_provider") or (model.partition("/")[0] if isinstance(model, str) else "")
api_key: Final = params.get("api_key")
if provider != "openai" or not isinstance(api_key, str) or not api_key:
return None
return (get_secret_str(api_key) if api_key.startswith("os.environ/") else api_key) or None
def codex_catalog_openai_key(
llm_router: Router | None,
entries: Sequence[tuple[str, str]],
team_id: str | None = None,
) -> str | None:
"""The OpenAI API key the catalog fetch authenticates with: the first one among the
listed entries' OpenAI deployments, else ``OPENAI_API_KEY`` from the environment."""
registry: Final = llm_router.registry() if llm_router is not None else None
deployments: Final = (
tuple(
deployment
for _, lookup_id in entries
for deployment in registry.deployments(
lookup_id, source="all", team_id=team_id
) # comprehension-ok: flattening the entries' deployment lists
)
if registry is not None
else ()
)
return next(filter(None, map(_deployment_openai_key, deployments)), None) or os.getenv("OPENAI_API_KEY") or None
def codex_catalog_rows(
rows: Sequence[ModelInfoResponse],
entries: Sequence[tuple[str, str]],

View file

@ -514,7 +514,7 @@ from litellm.proxy.common_utils.auth_cache_invalidation_pubsub import (
)
from litellm.proxy.common_utils.callback_utils import initialize_callbacks_on_proxy
from litellm.proxy.common_utils.codex_catalog_source import codex_catalog_source
from litellm.proxy.common_utils.codex_model_catalog import codex_model_list_body
from litellm.proxy.common_utils.codex_model_catalog import codex_catalog_openai_key, codex_model_list_body
from litellm.proxy.common_utils.config_includes import resolve_include_file_path, resolve_includes
from litellm.proxy.common_utils.config_sync_pubsub import ConfigSyncSubscriber
from litellm.proxy.common_utils.credential_hydration import decrypted_or_stored
@ -12815,7 +12815,12 @@ async def model_list(
llm_router,
team_id or user_api_key_dict.team_id,
client_version=client_version,
stock=await codex_catalog_source().stock(client_version),
stock=await codex_catalog_source().stock(
client_version,
openai_api_key=codex_catalog_openai_key(
llm_router, admin_entries, team_id or user_api_key_dict.team_id
),
),
),
media_type="application/json",
)
@ -12898,7 +12903,10 @@ async def model_list(
team_id or user_api_key_dict.team_id,
labels,
client_version=client_version,
stock=await codex_catalog_source().stock(client_version),
stock=await codex_catalog_source().stock(
client_version,
openai_api_key=codex_catalog_openai_key(llm_router, entries, team_id or user_api_key_dict.team_id),
),
),
media_type="application/json",
)

View file

@ -31,6 +31,11 @@ BUNDLED_STOCK_PATH: Final = ROOT / "litellm" / "proxy" / "common_utils" / "codex
BASE_INSTRUCTIONS_PATH: Final = ROOT / "litellm" / "proxy" / "common_utils" / "codex_base_instructions.md"
PUBLISHED_VERSION: Final = "0.999.0"
UNPUBLISHED_VERSION: Final = "0.998.0"
OPENAI_VERSION: Final = "0.997.0"
OPENAI_SLUG: Final = "gpt-openai-release"
OPENAI_PROMPT: Final = "# Base prompt OpenAI serves for its release"
OPENAI_MODELS_PATH: Final = "/codex/models"
DEPLOYMENT_KEY: Final = "integration-provider-key"
FRESH_SLUG: Final = "gpt-fresh-release"
FRESH_PROMPT: Final = "# Base prompt of the published release"
MODELS_PATH: Final = "codex-rs/models-manager/models.json"
@ -57,7 +62,21 @@ def _published_entry() -> dict[str, JsonValue]:
return {**_vendored_entry("gpt-5.5"), "slug": FRESH_SLUG, "display_name": "Fresh Release", "upgrade": None}
def _openai_entry() -> dict[str, JsonValue]:
return {
**_published_entry(),
"slug": OPENAI_SLUG,
"display_name": "OpenAI Release",
"base_instructions": OPENAI_PROMPT,
}
def _raw_tree(request: Request) -> Reply:
if request.target.startswith(f"{OPENAI_MODELS_PATH}?"):
served: Final = request.target == f"{OPENAI_MODELS_PATH}?client_version={OPENAI_VERSION}"
if served and request.headers.get("authorization") == f"Bearer {DEPLOYMENT_KEY}":
return Reply(body=json.dumps({"models": [_openai_entry()]}).encode())
return Reply(status=401, body=b'{"detail":"Unauthorized"}')
if request.target == f"/rust-v{PUBLISHED_VERSION}/{MODELS_PATH}":
return Reply(body=json.dumps({"models": [_published_entry()]}).encode())
if request.target == f"/rust-v{PUBLISHED_VERSION}/{PROMPT_PATH}":
@ -83,7 +102,11 @@ def raw_tree() -> Iterator[Wire]:
def gateway(tmp_path_factory: pytest.TempPathFactory, raw_tree: Wire) -> Iterator[Gateway]:
with gateway_from_environment() as parent_gateway:
directory: Final = tmp_path_factory.mktemp("codex_catalog_fetch")
overrides: Final = {"LITELLM_LOCAL_CODEX_CATALOG": "False", "LITELLM_CODEX_CATALOG_BASE_URL": raw_tree.url}
overrides: Final = {
"LITELLM_LOCAL_CODEX_CATALOG": "False",
"LITELLM_CODEX_CATALOG_BASE_URL": raw_tree.url,
"LITELLM_CODEX_CATALOG_OPENAI_URL": f"{raw_tree.url}{OPENAI_MODELS_PATH}",
}
config: Final = _config_without_model_list(directory)
with owned_proxy(parent_gateway, directory, overrides, config=config) as isolated_gateway:
yield isolated_gateway
@ -108,6 +131,28 @@ def _catalog(gateway: Gateway, client_version: str) -> dict[str, dict[str, JsonV
return {string_value(object_value(entry)["slug"]): object_value(entry) for entry in models}
def test_a_release_openai_serves_is_fetched_from_openai_with_the_deployment_key_and_github_is_skipped(
gateway: Gateway, raw_tree: Wire
) -> None:
assert OPENAI_SLUG not in _vendored_slugs()
with gateway.scenario() as scenario:
scenario.model(name=OPENAI_SLUG)
listing: Final = _catalog_listing(gateway, OPENAI_VERSION, OPENAI_SLUG)
expected: Final = {
**_openai_entry(),
"priority": listing[OPENAI_SLUG]["priority"],
"visibility": "list",
"supported_in_api": True,
"upgrade": None,
}
assert listing[OPENAI_SLUG] == expected, listing[OPENAI_SLUG]
requests: Final = raw_tree.drain()
openai_requests: Final = [request for request in requests if request.target.startswith(OPENAI_MODELS_PATH)]
assert len(openai_requests) == gateway.workers, [request.target for request in requests]
assert {request.headers.get("authorization") for request in openai_requests} == {f"Bearer {DEPLOYMENT_KEY}"}
assert not any(request.target.startswith(f"/rust-v{OPENAI_VERSION}/") for request in requests), requests
def test_a_published_release_gets_its_own_entry_for_a_model_the_vendored_pair_lacks(
gateway: Gateway, raw_tree: Wire
) -> None:

View file

@ -9,6 +9,7 @@ from pydantic import JsonValue, TypeAdapter
from litellm.proxy.common_utils.codex_catalog_source import (
CODEX_CATALOG_BASE_URL,
CODEX_CATALOG_OPENAI_URL,
CodexCatalogSource,
codex_release_tag,
vendored_codex_stock,
@ -44,9 +45,13 @@ class _RawTree:
def __init__(self, replies: Mapping[str, tuple[int, bytes]]) -> None:
self.replies = replies
self.calls: list[str] = []
self.headers: list[dict[str, str] | None] = []
async def get(self, url: str, *, timeout: float | None = None) -> httpx.Response:
async def get(
self, url: str, *, headers: dict[str, str] | None = None, timeout: float | None = None
) -> httpx.Response:
self.calls.append(url)
self.headers.append(headers)
status, body = self.replies.get(url, (404, b"404: Not Found"))
return httpx.Response(status, content=body)
@ -96,7 +101,7 @@ def test_the_requesting_release_catalog_serves_its_entry_for_a_model_the_vendore
tree = _RawTree(_tree(RELEASE))
stock = asyncio.run(_source(tree).stock(RELEASE))
entry = _entry(stock.models, stock.instructions, FRESH_SLUG)
assert stock.source == f"rust-v{RELEASE}"
assert stock.source == f"github rust-v{RELEASE}"
assert entry["shell_type"] == "shell_command" and entry["apply_patch_tool_type"] == "freeform", entry
assert entry["supports_parallel_tool_calls"] is True, entry
assert sorted(tree.calls) == sorted(_tree(RELEASE)), tree.calls
@ -111,7 +116,7 @@ def test_a_release_is_fetched_once_and_concurrent_requests_share_the_fetch() ->
again = await source.stock(RELEASE)
return [stock.source for stock in (*stocks, again)]
assert asyncio.run(burst()) == [f"rust-v{RELEASE}"] * 6
assert asyncio.run(burst()) == [f"github rust-v{RELEASE}"] * 6
assert len(tree.calls) == 2, tree.calls
@ -128,7 +133,7 @@ def test_a_release_without_a_tag_serves_the_vendored_pair_and_is_retried_after_t
assert len(tree.calls) == calls_after_first, "a failed fetch was retried inside its window"
clock.now += 2
tree.replies = _tree(RELEASE)
assert asyncio.run(source.stock(RELEASE)).source == f"rust-v{RELEASE}"
assert asyncio.run(source.stock(RELEASE)).source == f"github rust-v{RELEASE}"
assert len(tree.calls) > calls_after_first, "a failed fetch was never retried"
@ -152,7 +157,9 @@ def test_a_release_whose_pair_does_not_parse_serves_the_vendored_pair(replies: M
def test_a_transport_error_serves_the_vendored_pair() -> None:
class _Down:
async def get(self, url: str, *, timeout: float | None = None) -> httpx.Response:
async def get(
self, url: str, *, headers: dict[str, str] | None = None, timeout: float | None = None
) -> httpx.Response:
raise httpx.ConnectError("offline", request=httpx.Request("GET", url))
stock = asyncio.run(CodexCatalogSource(client=_Down(), clock=_Clock()).stock(RELEASE))
@ -191,7 +198,7 @@ def test_the_base_url_setting_points_the_fetch_at_a_mirror(monkeypatch: pytest.M
for url, reply in _tree(RELEASE).items()
}
tree = _RawTree(mirrored)
assert asyncio.run(_source(tree).stock(RELEASE)).source == f"rust-v{RELEASE}"
assert asyncio.run(_source(tree).stock(RELEASE)).source == f"github rust-v{RELEASE}"
assert sorted(tree.calls) == sorted(mirrored), tree.calls
@ -211,3 +218,36 @@ def test_the_cache_keeps_only_the_newest_releases() -> None:
newest_models_url = next(url for url in _tree(versions[-1]) if url.endswith("models.json"))
assert tree.calls.count(oldest_models_url) == 2, tree.calls
assert tree.calls.count(newest_models_url) == 1, tree.calls
OPENAI_KEY: Final = "sk-unit-openai-key"
OPENAI_ENTRY: Final = {**FRESH_ENTRY, "shell_type": "unified_exec", "base_instructions": "# OpenAI live prompt"}
OPENAI_CATALOG: Final = json.dumps({"models": [OPENAI_ENTRY]}).encode()
def _openai_url(version: str) -> str:
return f"{CODEX_CATALOG_OPENAI_URL}?client_version={version}"
def test_an_openai_key_fetches_the_live_catalog_from_openai_with_that_key_and_skips_github() -> None:
tree = _RawTree({**_tree(RELEASE), _openai_url(RELEASE): (200, OPENAI_CATALOG)})
stock = asyncio.run(_source(tree).stock(RELEASE, openai_api_key=OPENAI_KEY))
entry = _entry(stock.models, stock.instructions, FRESH_SLUG)
assert stock.source == f"openai {RELEASE}" and stock.instructions == "# OpenAI live prompt", stock.source
assert entry["shell_type"] == "unified_exec", entry
assert tree.calls == [_openai_url(RELEASE)] and tree.headers == [{"Authorization": f"Bearer {OPENAI_KEY}"}]
@pytest.mark.parametrize("status", [401, 500])
def test_a_refused_openai_fetch_falls_back_to_the_release_tag_on_github(status: int) -> None:
tree = _RawTree({**_tree(RELEASE), _openai_url(RELEASE): (status, b"{}")})
stock = asyncio.run(_source(tree).stock(RELEASE, openai_api_key=OPENAI_KEY))
assert stock.source == f"github rust-v{RELEASE}" and FRESH_SLUG in stock.models
assert tree.calls[0] == _openai_url(RELEASE) and sorted(tree.calls[1:]) == sorted(_tree(RELEASE)), tree.calls
def test_without_an_openai_key_openai_is_never_asked() -> None:
tree = _RawTree({**_tree(RELEASE), _openai_url(RELEASE): (200, OPENAI_CATALOG)})
stock = asyncio.run(_source(tree).stock(RELEASE))
assert stock.source == f"github rust-v{RELEASE}"
assert _openai_url(RELEASE) not in tree.calls and tree.headers == [None, None], tree.calls

View file

@ -14,6 +14,7 @@ from litellm.proxy.common_utils.codex_model_catalog import (
CodexStockModel,
CodexStockUpgrade,
bundled_codex_models,
codex_catalog_openai_key,
codex_catalog_rows,
codex_model_list_body,
codex_models_response_json,
@ -944,3 +945,36 @@ def test_codex_tool_search_is_offered_off_the_route_and_model_info_and_only_when
"mixed": False,
}
assert models[0]["context_window"] == 1000000
def _router_with(*litellm_params: Mapping[str, str]) -> litellm.Router:
return litellm.Router(
model_list=[
{"model_name": f"catalog-model-{index}", "litellm_params": dict(params)}
for index, params in enumerate(litellm_params)
]
)
def test_the_catalog_key_is_the_listed_openai_deployments_key_before_the_environments(
monkeypatch: pytest.MonkeyPatch,
) -> None:
monkeypatch.setenv("OPENAI_API_KEY", "sk-from-env")
monkeypatch.setenv("CATALOG_OPENAI_KEY", "sk-from-secret-ref")
router: Final = _router_with(
{"model": "anthropic/claude-sonnet-4-5", "api_key": "sk-ant-not-openai"},
{"model": "openai/gpt-5.5", "api_key": "os.environ/CATALOG_OPENAI_KEY"},
{"model": "openai/gpt-5.5", "api_key": "sk-second-openai"},
)
entries: Final = [(f"catalog-model-{index}", f"catalog-model-{index}") for index in range(3)]
assert codex_catalog_openai_key(router, entries) == "sk-from-secret-ref"
assert codex_catalog_openai_key(router, entries[:1]) == "sk-from-env"
def test_the_catalog_key_is_absent_without_an_openai_deployment_or_environment_key(
monkeypatch: pytest.MonkeyPatch,
) -> None:
monkeypatch.delenv("OPENAI_API_KEY", raising=False)
router: Final = _router_with({"model": "anthropic/claude-sonnet-4-5", "api_key": "sk-ant-not-openai"})
assert codex_catalog_openai_key(router, [("catalog-model-0", "catalog-model-0")]) is None
assert codex_catalog_openai_key(None, [("catalog-model-0", "catalog-model-0")]) is None