From e5831cb3a81b9b663f1beb3b48b4261c209f22fc Mon Sep 17 00:00:00 2001 From: Roland Tannous Date: Tue, 12 May 2026 09:24:33 +0400 Subject: [PATCH] studio/backend: forward top_k to Anthropic; filter OpenAI model list Two paired changes so the frontend capability map has matching backend behaviour: 1. ExternalProviderClient.stream_chat_completion now accepts top_k and forwards it to the Anthropic Messages body. OpenAI-compat providers (which all reject unknown sampling params) still receive only the fields they document. The proxy route in routes/inference.py passes payload.top_k through, so a UI request with top_k actually reaches Anthropic instead of being silently dropped at the boundary. 2. PROVIDER_REGISTRY['openai'] gains a model_id_allowlist regex that scopes the /models picker to current-gen ids (gpt-5.5 / gpt-5.4 / gpt-5.3 / gpt-4.5 / o3 families). The remote /v1/models listing otherwise returns dozens of historical snapshots, fine-tunes and non-chat models (embeddings, TTS, image, moderation) that we never want in the chat UI. default_models is refreshed to match. --- .../core/inference/external_provider.py | 11 ++++++++++- studio/backend/core/inference/providers.py | 18 ++++++++++++------ studio/backend/routes/inference.py | 1 + studio/backend/routes/providers.py | 3 +++ 4 files changed, 26 insertions(+), 7 deletions(-) diff --git a/studio/backend/core/inference/external_provider.py b/studio/backend/core/inference/external_provider.py index d1c612b4eb..a58b543279 100644 --- a/studio/backend/core/inference/external_provider.py +++ b/studio/backend/core/inference/external_provider.py @@ -67,6 +67,7 @@ class ExternalProviderClient: top_p: float = 0.95, max_tokens: Optional[int] = None, presence_penalty: float = 0.0, + top_k: Optional[int] = None, stream: bool = True, ) -> AsyncGenerator[str, None]: """ @@ -74,10 +75,15 @@ class ExternalProviderClient: For OpenAI-compatible providers, lines are forwarded verbatim. For Anthropic, the native Messages API SSE is translated to OpenAI format. + + ``top_k`` and ``presence_penalty`` are forwarded only when the caller + supplies a value the provider accepts — the frontend's + provider-capability map already filters these per provider, so we + treat them as opt-in here. """ if not self._is_openai_compatible(): async for line in self._stream_anthropic( - messages, model, temperature, top_p, max_tokens + messages, model, temperature, top_p, max_tokens, top_k ): yield line return @@ -180,6 +186,7 @@ class ExternalProviderClient: temperature: float, top_p: float, max_tokens: Optional[int], + top_k: Optional[int] = None, ) -> AsyncGenerator[str, None]: """ Call the Anthropic Messages API and translate its SSE to OpenAI format. @@ -257,6 +264,8 @@ class ExternalProviderClient: # Anthropic rejects requests that set both temperature and top_p "stream": True, } + if top_k is not None and top_k > 0: + body["top_k"] = top_k if system: body["system"] = system diff --git a/studio/backend/core/inference/providers.py b/studio/backend/core/inference/providers.py index 0cd80f4718..8d1ad82c87 100644 --- a/studio/backend/core/inference/providers.py +++ b/studio/backend/core/inference/providers.py @@ -8,6 +8,7 @@ All providers expose OpenAI-compatible /v1/chat/completions endpoints with Bearer token authentication and SSE streaming support. """ +import re from typing import Any PROVIDER_REGISTRY: dict[str, dict[str, Any]] = { @@ -15,18 +16,23 @@ PROVIDER_REGISTRY: dict[str, dict[str, Any]] = { "display_name": "OpenAI", "base_url": "https://api.openai.com/v1", "default_models": [ - "gpt-4o", - "gpt-4o-mini", - "gpt-4.1", - "gpt-4.1-mini", - "gpt-4.1-nano", - "o3-mini", + "gpt-5.5", + "gpt-5.4", + "gpt-5.4-mini", + "gpt-5.3", + "o3", ], "supports_streaming": True, "supports_vision": True, "supports_tool_calling": True, "auth_header": "Authorization", "auth_prefix": "Bearer ", + # Keep the model picker scoped to the current generation. The remote + # /v1/models listing returns dozens of historical snapshots, fine-tunes + # and non-chat models (embeddings, TTS, image, moderation) that we + # never want to surface in the chat UI. Filtering here so backend + # is the single source of truth. + "model_id_allowlist": re.compile(r"^(gpt-5\.[345]|gpt-4\.5|o3)(?:[-.]|$)"), }, "anthropic": { "display_name": "Anthropic", diff --git a/studio/backend/routes/inference.py b/studio/backend/routes/inference.py index 2df23d9f78..c9d385a503 100644 --- a/studio/backend/routes/inference.py +++ b/studio/backend/routes/inference.py @@ -1592,6 +1592,7 @@ async def _proxy_to_external_provider( top_p = payload.top_p, max_tokens = payload.max_tokens, presence_penalty = payload.presence_penalty, + top_k = payload.top_k, stream = payload.stream, ) try: diff --git a/studio/backend/routes/providers.py b/studio/backend/routes/providers.py index 0867b491ff..d239de8104 100644 --- a/studio/backend/routes/providers.py +++ b/studio/backend/routes/providers.py @@ -281,6 +281,9 @@ async def list_provider_models( try: models = await client.list_models() + allowlist = info.get("model_id_allowlist") + if allowlist is not None: + models = [m for m in models if allowlist.match(m.get("id", ""))] return [ ProviderModelInfo( id = m.get("id", ""),