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:
parent
f40b5fbe75
commit
e5831cb3a8
4 changed files with 26 additions and 7 deletions
|
|
@ -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
|
||||
|
||||
|
|
|
|||
|
|
@ -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",
|
||||
|
|
|
|||
|
|
@ -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:
|
||||
|
|
|
|||
|
|
@ -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", ""),
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue