diff --git a/studio/backend/core/inference/providers.py b/studio/backend/core/inference/providers.py index 850566757f..018f3786ed 100644 --- a/studio/backend/core/inference/providers.py +++ b/studio/backend/core/inference/providers.py @@ -36,22 +36,21 @@ PROVIDER_REGISTRY: dict[str, dict[str, Any]] = { # # Pattern strategy: # - Unambiguous *feature* indicators (tts/whisper/transcribe/ - # realtime/image/embedding/moderation/sora) match anywhere - # in the id with `(?:^|-)` because OpenAI uses them as the - # primary qualifier on every variant -- e.g. canonical - # `tts-1` AND family variants `gpt-4o-tts`, `gpt-4o-mini-tts`. - # These words don't appear in legitimate chat model ids - # mid-string, so the mid-id match is intentional and safe. - # - `audio` is INTENTIONALLY NOT in the mid-id set: the - # `gpt-audio` / `gpt-audio-mini` / `gpt-audio-1.5` family is - # chat-completion-capable (text in / text or audio out via - # /v1/chat/completions and /v1/responses), so dropping them - # would hide supported chat models from the picker. The + # image/embedding/moderation/sora) match anywhere in the id + # with `(?:^|-)` because OpenAI uses them as the primary + # qualifier on every variant -- e.g. canonical `tts-1` AND + # family variants `gpt-4o-tts`, `gpt-4o-mini-tts`. These + # words don't appear in legitimate chat model ids mid-string, + # so the mid-id match is intentional and safe. + # - `audio` and `realtime` are INTENTIONALLY NOT in the + # mid-id set. Both the `gpt-audio*` and `gpt-realtime*` + # families are chat/responses-capable today -- per OpenAI's + # audio and realtime guides they accept text in via + # /v1/chat/completions and /v1/responses, not only the + # specialised /v1/realtime WebSocket transport. Dropping + # them hid supported chat models from the picker. The # audio-only endpoint families (`tts-*`, `whisper-*`, - # `*-transcribe`) ARE caught above. - # - `realtime` keeps the mid-id match because the - # `gpt-realtime*` family is a separate /v1/realtime - # (WebSocket) endpoint, not chat-completions. + # `*-transcribe`) are still caught above. # - `search` is INTENTIONALLY NOT in the mid-id set because # `gpt-4o-search-preview` and `gpt-4o-mini-search-preview` are # chat-with-retrieval models that absolutely belong in the @@ -69,9 +68,10 @@ PROVIDER_REGISTRY: dict[str, dict[str, Any]] = { # Verified against the live /v1/models listing 2026-05-22. "model_id_denylist": re.compile( # Feature suffixes that mark a non-chat variant on any base. - # Note: `audio` is omitted on purpose -- see docstring above. + # Note: `audio` and `realtime` are omitted on purpose -- + # see docstring above. r"(?:^|-)(?:embedding|tts|whisper|moderation|image|" - r"realtime|transcribe|sora)\b" + r"transcribe|sora)\b" # Legacy completion bases -- ^-anchored to avoid false # positives on hypothetical future chat ids containing # those words mid-string. diff --git a/studio/backend/tests/test_provider_registry_filters.py b/studio/backend/tests/test_provider_registry_filters.py index c055739c30..f8451e67ff 100644 --- a/studio/backend/tests/test_provider_registry_filters.py +++ b/studio/backend/tests/test_provider_registry_filters.py @@ -77,27 +77,39 @@ def test_openai_keeps_every_known_chat_family(): assert surviving == live, surviving -def test_openai_audio_family_is_chat_capable_and_kept(): +def test_openai_audio_and_realtime_families_are_chat_capable_and_kept(): # Pin the chat/non-chat split that lives in the regex comment: - # gpt-audio is kept (chat completions accept it), gpt-realtime is - # dropped (separate /v1/realtime endpoint), gpt-4o-transcribe is - # dropped (audio-only input via /v1/audio/transcriptions). + # gpt-audio AND gpt-realtime are kept (both accept text in via + # /v1/chat/completions and /v1/responses, not only their + # specialised transports); gpt-4o-transcribe etc. are dropped + # (audio-only input via /v1/audio/transcriptions). kept = _apply( "openai", [ "gpt-audio", "gpt-audio-1.5", "gpt-audio-mini", + "gpt-realtime", + "gpt-realtime-mini", + "gpt-realtime-1.5", + "gpt-realtime-2", + "gpt-4o-realtime-preview", ], ) - assert kept == ["gpt-audio", "gpt-audio-1.5", "gpt-audio-mini"], kept + assert kept == [ + "gpt-audio", + "gpt-audio-1.5", + "gpt-audio-mini", + "gpt-realtime", + "gpt-realtime-mini", + "gpt-realtime-1.5", + "gpt-realtime-2", + "gpt-4o-realtime-preview", + ], kept dropped = _apply( "openai", [ - "gpt-realtime", - "gpt-realtime-mini", - "gpt-4o-realtime-preview", "gpt-4o-transcribe", "gpt-4o-mini-transcribe", ], @@ -108,8 +120,9 @@ def test_openai_audio_family_is_chat_capable_and_kept(): def test_openai_drops_non_chat_ids(): noise = [ # Embeddings / TTS / image / moderation / whisper / etc. - # gpt-audio* is intentionally OMITTED from this list -- it is - # chat-capable. See test_openai_audio_family_is_chat_capable_and_kept. + # gpt-audio* and gpt-realtime* are intentionally OMITTED from + # this list -- both are chat-capable. See + # test_openai_audio_and_realtime_families_are_chat_capable_and_kept. "text-embedding-3-small", "text-embedding-3-large", "text-embedding-ada-002", @@ -125,8 +138,6 @@ def test_openai_drops_non_chat_ids(): "gpt-image-2", "gpt-image-1-mini", "chatgpt-image-latest", - "gpt-realtime-2", - "gpt-4o-realtime-preview", "gpt-4o-transcribe", "gpt-4o-mini-transcribe", "gpt-4o-mini-tts",