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.
This commit is contained in:
Roland Tannous 2026-05-12 09:24:33 +04:00
commit e5831cb3a8
4 changed files with 26 additions and 7 deletions

View file

@ -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

View file

@ -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",

View file

@ -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:

View file

@ -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", ""),