Tighten comments across PR 5711 (no behaviour change)

Audited every comment added by this PR; condensed multi-paragraph
docstrings and inline blocks to 1-2 lines where the WHY survives.

Files touched: 6 backend + 7 frontend. Net -148 lines.
- external_provider.py: -80 (docstring + stop_sequences + compaction)
- chat-settings-sheet.tsx: -70 (InfoHint tooltips + section headers)
- routes/inference.py: -54 (parallel_tool_calls cap comments)
- provider-capabilities.ts: -108 (stop-cap table, gemini bucket,
  service_tier resolver, max-output cap header)
- chat-adapter.ts: -27 (sampling forwarding stanza)
- providers.py: -24 (kimi reasoning class, deepseek aliases)
- llama_cpp.py: -24 (payload builder shared header)
- models/inference.py: -28 (Pydantic Field descriptions)
- chat-settings-storage.ts: -23 (nullable handling comments)
- stop-sequences-input.tsx: -23 (JSDoc + chip-key + draft-commit)
- anthropic_compat.py: -9 (serial tool-call gate)
- types/runtime.ts: -12 (DRY JSDoc, null convention header)
- types/api.ts: -6 (service_tier JSDoc)

Tightening rules applied:
- Removed em-dashes everywhere a comment was rewritten.
- Kept every external doc URL (Anthropic, OpenAI, vLLM, llama.cpp,
  Ollama, OpenRouter, Mistral, DeepSeek, Gemini, Kimi).
- Collapsed "silently dropped on unsupported routes" prose since the
  bucket-comment at the top of each capability table already states it.
- Removed restated rationale prose in comment blocks where the field
  name plus the master rule already encodes the WHY.

393/393 backend tests pass (test_sampling_params_routing 65, plus
anthropic / openai / gemini / llama-server suites). Frontend tsc +
vite build clean.
This commit is contained in:
Daniel Han 2026-05-27 14:32:11 +00:00
commit 1add4dbd0e
13 changed files with 170 additions and 318 deletions

View file

@ -410,11 +410,10 @@ class AnthropicPassthroughEmitter:
self._tool_call_states: dict = {} # delta index -> {block_index, id, name}
self._usage: dict = {}
self._stop_reason: str = "end_turn"
# When the caller opted out of parallel tool calls (Anthropic
# `tool_choice.disable_parallel_tool_use=true` mapped to local
# `parallel_tool_calls=false`), only emit the first tool-call
# index and drop later siblings. llama.cpp may not enforce the
# flag on every jinja template (ggml-org/llama.cpp#22043).
# parallel_tool_calls=False (Anthropic
# tool_choice.disable_parallel_tool_use=true): emit only the first
# tool-call index; llama.cpp's flag isn't enforced by every jinja
# template (ggml-org/llama.cpp#22043).
self._serial_tool_calls: bool = parallel_tool_calls is False
self._first_tool_call_idx: Optional[int] = None

View file

@ -81,15 +81,10 @@ def _normalize_stop_for_provider(
def _is_openai_family_cloud(base_url: Optional[str]) -> bool:
"""True iff ``base_url`` points at OpenAI cloud or Azure OpenAI Foundry.
Host-anchored to avoid subdomain-injection bypass
(https://evil.com/api.openai.com/v1, https://api.openai.com.attacker.com/v1).
Used to gate cloud-only Responses-API extensions
(prompt_cache_retention, context_management compaction, container
shell tool) that 400 on non-cloud OAI-compat servers.
Azure Foundry uses <resource>.openai.azure.com; match via endswith
with the leading dot so the apex `openai.azure.com` can't slip
through (no apex Foundry endpoint exists).
Host-anchored against subdomain-injection (api.openai.com.attacker.com).
Gates Responses-API extensions (prompt_cache_retention, context_management,
container shell) that 400 on non-cloud OAI-compat servers. Azure Foundry
matches via .openai.azure.com suffix; leading dot blocks the apex.
"""
if not base_url:
return False
@ -917,19 +912,14 @@ 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.
Optional sampling extras (``top_k``, ``presence_penalty``,
``frequency_penalty``, ``seed``, ``stop``, ``service_tier``,
``parallel_tool_calls`` follow the same rule: the per-provider
stream helpers silently drop fields the upstream API does not
accept (e.g. Responses rejects all of seed / frequency / stop;
Anthropic does not implement seed / frequency / logprobs).
``parallel_tool_calls``) are opt-in. Per-provider helpers silently
drop fields the upstream rejects (Responses: seed/freq/stop;
Anthropic: seed/freq/logprobs).
``fast_mode`` only applies to Anthropic Opus 4.6 / 4.7 (silently
dropped elsewhere); adds the beta header and ``speed: "fast"``.
``fast_mode``: Anthropic Opus 4.6/4.7 only; adds the beta header
and ``speed: "fast"``.
"""
# tool_choice="none" hard-disables hosted/builtin tools across
# every provider so enabled_tools cannot accidentally bill or leak.
@ -1054,9 +1044,8 @@ class ExternalProviderClient:
else:
body["max_tokens"] = max_tokens
# Optional sampling extensions. Only forwarded when the caller
# passed a value. Per-provider rename / cap via `seed_field` /
# `stop_max`; `body_omit` strips fields the upstream rejects.
# Optional sampling extras; `seed_field` renames seed (Mistral),
# `body_omit` strips upstream-rejected fields.
from core.inference.providers import get_provider_info
provider_info = get_provider_info(self.provider_type) or {}
@ -1069,9 +1058,8 @@ class ExternalProviderClient:
normalized_stop = _normalize_stop_for_provider(stop, provider_info)
if normalized_stop:
body["stop"] = normalized_stop
# service_tier is OpenAI Chat-only on the generic OAI-compat
# branch; opt-in via `accepts_service_tier=True` on the registry
# entry. Anthropic and Responses handle it in their own helpers.
# service_tier is OAI-Chat-only here (accepts_service_tier registry
# opt-in); Anthropic/Responses branches set it themselves.
if service_tier is not None and provider_info.get(
"accepts_service_tier", False
):
@ -1468,10 +1456,8 @@ class ExternalProviderClient:
if max_tokens is not None:
body["max_tokens"] = max_tokens
# The default OAI-compat body construction is skipped because
# this helper returns early. Apply the same provider-aware
# sampling / stop logic here so kimi-with-search matches
# kimi-without-search.
# Kimi-with-search returns early before the default OAI-compat
# body build; re-apply provider-aware sampling/stop here.
from core.inference.providers import get_provider_info
provider_info = get_provider_info(self.provider_type) or {}
@ -2141,27 +2127,17 @@ class ExternalProviderClient:
if top_k is not None and top_k > 0 and not sampling_removed:
body["top_k"] = top_k
# Optional sampling extensions. Anthropic has no
# frequency_penalty / seed / logprobs equivalents so they are
# never forwarded here. The two body-level knobs Anthropic
# accepts land here:
# stop -> stop_sequences (renamed, ws-stripped)
# service_tier -> service_tier (auto|standard_only only)
# parallel_tool_calls inversion is applied after the tools
# wiring below because Anthropic requires it nested under
# tool_choice.
# Anthropic body-knob mapping: stop -> stop_sequences (ws-stripped,
# dedup), service_tier (auto|standard_only). parallel_tool_calls is
# handled after tools wiring (Anthropic nests under tool_choice).
if stop:
sequences: list[str]
if isinstance(stop, str):
sequences = [stop] if stop.strip() else []
else:
# Dedupe + drop whitespace-only entries. Anthropic
# rejects any sequence with no non-whitespace char
# ("stop_sequences: each stop sequence must contain
# non-whitespace"), so "", " ", "\n", "\n\n" are all
# filtered. The 16-cap is a client-side guard; the
# docs do not publish a max, but every SDK treats 16
# as a sane ceiling (Bedrock is the outlier at 8191).
# Anthropic rejects whitespace-only stop_sequences ("must
# contain non-whitespace"); 16-cap is a client-side guard
# (undocumented max; every SDK uses 16, Bedrock at 8191).
sequences = list(
dict.fromkeys(s for s in stop if isinstance(s, str) and s.strip())
)
@ -2371,9 +2347,8 @@ class ExternalProviderClient:
if anthropic_code_exec_container_id:
body["container"] = anthropic_code_exec_container_id
# parallel_tool_calls=False maps to disable_parallel_tool_use=True
# nested under tool_choice (top-level placement is rejected).
# Without tools the flag is a no-op so we skip the block.
# parallel_tool_calls=False -> tool_choice.disable_parallel_tool_use=True
# (top-level 400s; no-op without tools).
# https://platform.claude.com/docs/en/agents-and-tools/tool-use/implement-tool-use
if parallel_tool_calls is False and body.get("tools"):
tc = body.get("tool_choice")
@ -5435,11 +5410,8 @@ class ExternalProviderClient:
"input": input_items,
"stream": True,
}
# Responses accepts auto|default|flex|priority per the live
# docs. The openai-python SDK type happens to include "scale"
# too but the public Responses reference does not, so drop it
# here to avoid a 400. Scale Tier is still selectable on Chat
# Completions backends. parallel_tool_calls default is true.
# Responses: auto|default|flex|priority (SDK type lists "scale"
# but server 400s; Scale Tier stays on Chat Completions).
if service_tier in ("auto", "default", "flex", "priority"):
body["service_tier"] = service_tier
if parallel_tool_calls is not None:

View file

@ -4316,10 +4316,8 @@ class LlamaCppBackend:
else (self._effective_context_length or _DEFAULT_MAX_TOKENS_FLOOR)
)
payload["t_max_predict_ms"] = _DEFAULT_T_MAX_PREDICT_MS
# Strip empty / non-string stop entries before forwarding to
# llama-server (matches `_normalize_stop_for_provider` for the
# external path). Without this a stale `stop=["", "END"]` can
# 400 the upstream.
# Strip empty/non-string stop entries (mirrors
# `_normalize_stop_for_provider`); stale `stop=["", "END"]` 400s.
if stop:
if isinstance(stop, str):
payload["stop"] = stop
@ -4327,9 +4325,8 @@ class LlamaCppBackend:
_cleaned = [s for s in stop if isinstance(s, str) and s]
if _cleaned:
payload["stop"] = _cleaned
# Each field gated `is not None` so explicit 0 / 0.0 / False
# values reach the wire. llama-server silently ignores fields
# it doesn't recognise.
# `is not None` gate so explicit 0/False reach the wire;
# llama-server ignores unknown fields.
if frequency_penalty is not None:
payload["frequency_penalty"] = frequency_penalty
if seed is not None:
@ -5195,12 +5192,9 @@ class LlamaCppBackend:
_accumulated_predicted_ms += _it.get("predicted_ms", 0)
_accumulated_predicted_n += _it.get("predicted_n", 0)
# When the caller opted out of parallel tool calls
# (parallel_tool_calls=False), enforce at most one call
# per assistant turn even if llama-server emitted more.
# llama.cpp's parallel_tool_calls flag isn't enforced by
# every jinja template (see ggml-org/llama.cpp#22043),
# so this client-side cap is the only guarantee.
# parallel_tool_calls=False: client-side cap to 1
# (llama.cpp flag isn't enforced by every jinja template,
# ggml-org/llama.cpp#22043).
if parallel_tool_calls is False and tool_calls:
tool_calls = tool_calls[:1]
@ -5414,8 +5408,8 @@ class LlamaCppBackend:
_cleaned = [s for s in stop if isinstance(s, str) and s]
if _cleaned:
stream_payload["stop"] = _cleaned
# Match the per-iteration tool loop above so sampling behavior
# stays consistent when the cap-exhausted final-answer pass runs.
# Match per-iteration tool-loop sampling for the cap-exhausted
# final-answer pass.
if frequency_penalty is not None:
stream_payload["frequency_penalty"] = frequency_penalty
if seed is not None:

View file

@ -137,19 +137,17 @@ PROVIDER_REGISTRY: dict[str, dict[str, Any]] = {
r"gemini-pro-latest|gemini-flash-latest|gemini-flash-lite-latest"
r")$"
),
# Gemini's OpenAI-compatible layer inherits OpenAI's 4-stop cap
# (https://ai.google.dev/gemini-api/docs/openai). Without the
# explicit cap the default 16 leaks through and the upstream
# silently drops the overflow.
# Gemini OAI-compat inherits OpenAI's 4-stop cap; default 16
# silently truncates upstream.
# https://ai.google.dev/gemini-api/docs/openai
"stop_max": 4,
},
"deepseek": {
"display_name": "DeepSeek",
"base_url": "https://api.deepseek.com/v1",
# Legacy aliases (deepseek-chat / deepseek-reasoner) retire
# 2026-07-24 per https://api-docs.deepseek.com/updates. Surface
# the new canonical ids (deepseek-v4-flash / deepseek-v4-pro)
# alongside so the picker keeps working on cutover.
# deepseek-chat / deepseek-reasoner retire 2026-07-24; list
# v4-pro / v4-flash alongside for cutover.
# https://api-docs.deepseek.com/updates
"default_models": [
"deepseek-v4-pro",
"deepseek-v4-flash",
@ -217,13 +215,9 @@ PROVIDER_REGISTRY: dict[str, dict[str, Any]] = {
"auth_prefix": "Bearer ",
"notes": "Moonshot API key. China: use base URL https://api.moonshot.cn/v1",
"model_id_allowlist": re.compile(r"^kimi-k2\.[56]$"),
# Both k2.6 and k2.5 are reasoning-class. The API rejects
# custom sampling ("invalid temperature: only 1 is allowed for
# this model", same for top_p). frequency_penalty follows the
# same lock on those models. seed and parallel_tool_calls are
# not in Kimi's documented chat schema; strip them too so a
# stale client or direct API caller cannot smuggle them onto
# the wire and 400 the request.
# k2.5/k2.6 are reasoning-class: API locks temperature=1, top_p=1,
# frequency_penalty; seed and parallel_tool_calls are undocumented
# and 400.
"body_omit": (
"temperature",
"top_p",

View file

@ -876,10 +876,8 @@ class ChatCompletionRequest(BaseModel):
seed: Optional[int] = Field(
None,
description = (
"Best-effort determinism seed. Forwarded to OpenAI Chat "
"Completions and OpenAI-compatible local backends. The "
"Responses family rejects it server-side and Anthropic does "
"not implement it, so it is silently dropped on those routes."
"Best-effort determinism seed. Forwarded to OpenAI Chat and "
"OAI-compat local backends; dropped on Anthropic and OpenAI Responses."
),
)
service_tier: Optional[
@ -887,22 +885,18 @@ class ChatCompletionRequest(BaseModel):
] = Field(
None,
description = (
"Provider service tier. Anthropic accepts only `auto` and "
"`standard_only`; OpenAI Chat accepts "
"`auto|default|flex|priority|scale`; OpenAI Responses accepts "
"`auto|default|flex|priority`. Unsupported values per provider "
"are dropped in the per-provider stream helper instead of "
"422'ing here so a stale frontend never breaks a request."
"Provider service tier. Anthropic: auto|standard_only. "
"OpenAI Chat: auto|default|flex|priority|scale. "
"OpenAI Responses: auto|default|flex|priority. "
"Unsupported values are dropped per provider rather than 422'd here."
),
)
parallel_tool_calls: Optional[bool] = Field(
None,
description = (
"Whether the provider may dispatch tool calls in parallel. "
"OpenAI: forwarded as `parallel_tool_calls`. Anthropic: "
"inverted into `disable_parallel_tool_use` on the Messages "
"body. Default `None` preserves each provider's upstream "
"default (which is `true` everywhere today)."
"Allow parallel tool calls. Forwarded as `parallel_tool_calls` "
"on OpenAI; inverted to `disable_parallel_tool_use` on Anthropic. "
"None preserves upstream default (currently true everywhere)."
),
)
typical_p: Optional[float] = Field(
@ -958,8 +952,8 @@ class ChatCompletionRequest(BaseModel):
None,
ge = 0.0,
description = (
"llama.cpp DRY multiplier. 0 disables the 4-field chain "
"(dry_base / dry_allowed_length / dry_penalty_last_n). Local only."
"llama.cpp DRY multiplier. 0 disables the dry_base / "
"dry_allowed_length / dry_penalty_last_n chain. Local only."
),
)
dry_base: Optional[float] = Field(

View file

@ -240,12 +240,9 @@ studio_router = APIRouter()
def _clean_local_stop_list(stop) -> Optional[list[str]]:
"""Strip empty / non-string entries from a stop sequence input.
Mirrors `_normalize_stop_for_provider` (external_provider.py) so
local llama-server callers cannot ship `stop=["", "END"]` and
get a 400. Returns `None` when nothing survives, so the caller
can omit the field entirely.
"""Strip empty/non-string stop entries; returns None when empty so
callers can omit. Mirrors `_normalize_stop_for_provider` so
`stop=["", "END"]` cannot 400 llama-server.
"""
if isinstance(stop, str):
return [stop] if stop else None
@ -4185,9 +4182,7 @@ def _build_chat_request(
chat_kwargs["top_p"] = payload.top_p
if payload.max_output_tokens is not None:
chat_kwargs["max_tokens"] = payload.max_output_tokens
# parallel_tool_calls is first-class on ChatCompletionRequest and
# the OpenAI-compat passthrough builder forwards it. Translate it
# so a Responses API caller's preference reaches llama-server.
# Forward parallel_tool_calls from Responses caller through to llama-server.
if payload.parallel_tool_calls is not None:
chat_kwargs["parallel_tool_calls"] = payload.parallel_tool_calls
@ -4250,9 +4245,8 @@ async def _responses_non_streaming(
msg = choices[0].get("message", {}) or {}
text = msg.get("content", "") or ""
tool_calls = msg.get("tool_calls") or []
# Match the cap applied on GGUF / Anthropic / safetensors tool
# paths: when the caller opted out of parallel tool calls, surface
# at most one. llama.cpp may not enforce the flag.
# parallel_tool_calls=False -> cap to 1 (llama.cpp flag isn't enforced;
# mirrors GGUF/Anthropic/safetensors paths).
if payload.parallel_tool_calls is False and tool_calls:
tool_calls = tool_calls[:1]
@ -4376,10 +4370,9 @@ async def _responses_stream(
tool_call_state: dict[int, dict] = {}
# Text message lives at output_index 0; tool calls claim 1, 2, ...
next_output_index = 1
# When the caller opted out of parallel tool calls, latch the
# first index we see and drop subsequent siblings — mirrors the
# GGUF agentic-loop / Anthropic-passthrough caps; llama.cpp may
# not enforce the upstream flag (ggml-org/llama.cpp#22043).
# parallel_tool_calls=False: latch the first tc index, drop the
# rest; llama.cpp flag isn't enforced by every jinja template
# (ggml-org/llama.cpp#22043).
serial_tool_calls = payload.parallel_tool_calls is False
first_serial_idx: Optional[int] = None
@ -4872,10 +4865,9 @@ async def anthropic_messages(
if openai_tool_choice is None:
openai_tool_choice = "auto"
# Anthropic nests `disable_parallel_tool_use` inside `tool_choice`
# (https://docs.claude.com/en/docs/agents-and-tools/tool-use/implement-tool-use).
# Flip it into the OpenAI-shaped `parallel_tool_calls` toggle so the
# local GGUF tool loop respects clients that opt out of parallel calls.
# Anthropic nests `disable_parallel_tool_use` under `tool_choice`;
# flip to OAI `parallel_tool_calls` so the local GGUF tool loop honors it.
# https://docs.claude.com/en/docs/agents-and-tools/tool-use/implement-tool-use
anthropic_parallel_tool_calls: Optional[bool] = None
if isinstance(payload.tool_choice, dict):
_disable = payload.tool_choice.get("disable_parallel_tool_use")
@ -5393,10 +5385,8 @@ def _build_passthrough_payload(
else (backend_ctx or _DEFAULT_MAX_TOKENS_FLOOR)
)
body["t_max_predict_ms"] = _DEFAULT_T_MAX_PREDICT_MS
# Strip empty / non-string stop entries before forwarding to
# llama-server; the external-provider helper does this via
# `_normalize_stop_for_provider`, and the local path needs the same
# defensive shape so a stale `stop=["", "END"]` cannot 400 upstream.
# Strip empty stop entries (mirrors `_normalize_stop_for_provider`);
# stale `stop=["", "END"]` would 400 llama-server.
if stop:
if isinstance(stop, str):
if stop:
@ -5412,12 +5402,9 @@ def _build_passthrough_payload(
body["repeat_penalty"] = repetition_penalty
if presence_penalty is not None:
body["presence_penalty"] = presence_penalty
# llama-server's /v1/chat/completions accepts the standard OpenAI
# fields. parallel_tool_calls is a no-op on llama-server today but
# forwarded so a future release picks it up automatically.
# Each field below gated `is not None` so explicit 0 / False reach
# the wire; llama-server silently ignores unknown fields, Ollama's
# OAI translator drops everything outside the OAI subset.
# parallel_tool_calls is a no-op on llama-server today but forwarded
# for future support. `is not None` gate lets explicit 0/False through;
# llama-server ignores unknowns, Ollama drops non-OAI fields.
if frequency_penalty is not None:
body["frequency_penalty"] = frequency_penalty
if seed is not None:
@ -5705,11 +5692,8 @@ async def _anthropic_passthrough_non_streaming(
content_blocks.append(AnthropicResponseTextBlock(text = text))
tool_calls = message.get("tool_calls") or []
# Mirror the GGUF agentic-loop client-side cap: when the caller
# opted out of parallel tool calls, surface at most one tool_use
# block even if llama-server returned more than one. llama.cpp may
# not enforce the flag on every jinja template (see
# ggml-org/llama.cpp#22043).
# parallel_tool_calls=False: cap to 1 tool_use block; llama.cpp flag
# isn't enforced by every jinja template (ggml-org/llama.cpp#22043).
if parallel_tool_calls is False and tool_calls:
tool_calls = tool_calls[:1]
for tc in tool_calls:

View file

@ -7,14 +7,8 @@ import { cn } from "@/lib/utils";
import { XIcon } from "lucide-react";
import { type KeyboardEvent, useState } from "react";
/**
* Chips editor for the `stop` / `stop_sequences` array.
*
* Commit a chip with Enter or comma; press Backspace on an empty input
* to delete the most recent chip. Caps at `maxEntries` -- OpenAI Chat
* Completions documents a hard cap of 4 stop sequences; Anthropic
* Messages accepts arbitrarily many. Pass `Infinity` to disable the cap.
*/
/** Chips editor for stop/stop_sequences. Enter or comma commits; Backspace
* on empty deletes the last. OpenAI Chat caps at 4; pass Infinity to disable. */
export interface StopSequencesInputProps {
value: string[];
onChange: (next: string[]) => void;
@ -38,12 +32,9 @@ export function StopSequencesInput({
const atCap = value.length >= maxEntries;
function commitDraft() {
// Reject only the empty draft; preserve whitespace exactly (stop
// matching is byte-exact). OpenAI-compat / llama-server backends
// accept whitespace-only stops like "\n\n" for blank-line halts;
// pasting such a value into the input should round-trip rather
// than be silently dropped. Anthropic's helper strips whitespace
// entries on the wire so a chip that's invalid there cannot 400.
// Preserve whitespace exactly (stops are byte-exact); llama-server
// accepts "\n\n" for blank-line halts. Anthropic strips whitespace
// entries on the wire.
if (!draft) return;
if (atCap) return;
if (value.includes(draft)) {
@ -85,9 +76,7 @@ export function StopSequencesInput({
>
{value.map((entry, index) => (
<Badge
// Stop sequences are not guaranteed unique across edits (the
// user could enter "END" twice if the previous one was just
// deleted), so combine value + index for a stable key.
// Not unique across edits (re-typed value); combine value+index for a stable key.
key={`${entry}-${index}`}
variant="secondary"
className="gap-1 pl-2 pr-1"

View file

@ -1991,10 +1991,8 @@ export function createOpenAIStreamAdapter(): ChatModelAdapter {
...(externalCapabilities?.presencePenalty
? { presence_penalty: params.presencePenalty }
: {}),
// Optional sampling extensions. Each gate is per-provider
// (see provider-capabilities.ts); the backend additionally
// drops fields the upstream API does not accept, so a
// stale frontend cannot 400 the request.
// Optional sampling extras. Per-provider gates live in
// provider-capabilities.ts; backend drops unknown fields.
...(externalCapabilities?.frequencyPenalty
? { frequency_penalty: params.frequencyPenalty }
: {}),
@ -2007,16 +2005,14 @@ export function createOpenAIStreamAdapter(): ChatModelAdapter {
...(externalCapabilities?.serviceTier && params.serviceTier
? { service_tier: params.serviceTier }
: {}),
// Forward parallel_tool_calls when the user explicitly
// turned it off (the upstream default is `true` on every
// provider we ship, so default true is a no-op).
// Upstream default is true on every shipped provider;
// forward only on explicit opt-out.
...(externalCapabilities?.parallelToolCalls &&
params.parallelToolCalls === false
? { parallel_tool_calls: false }
: {}),
// llama.cpp / vLLM / OpenRouter extras. Each is gated by
// (a) the active provider's capability flag and (b) a
// non-default value, so only meaningful knobs hit the wire.
// llama.cpp / vLLM / OpenRouter extras: cap-flag plus
// non-default value gates so only meaningful knobs hit wire.
...(externalCapabilities?.typicalP &&
params.typicalP !== null &&
params.typicalP !== 1
@ -2244,10 +2240,9 @@ export function createOpenAIStreamAdapter(): ChatModelAdapter {
min_p: params.minP,
repetition_penalty: params.repetitionPenalty,
presence_penalty: params.presencePenalty,
// llama-server accepts the standard OAI extensions via
// _build_passthrough_payload and silently ignores unknown
// fields. parallel_tool_calls defaults to false upstream so
// we forward unconditionally to honour the default-on UI.
// llama-server (_build_passthrough_payload) accepts OAI
// extras and ignores unknown; parallel_tool_calls default
// false upstream so forward unconditionally.
...(params.frequencyPenalty !== 0
? { frequency_penalty: params.frequencyPenalty }
: {}),
@ -2312,9 +2307,7 @@ export function createOpenAIStreamAdapter(): ChatModelAdapter {
...(params.minTokens !== null && params.minTokens > 0
? { min_tokens: params.minTokens }
: {}),
// Forward only when value diverges from upstream default;
// per-backend capability gating decides whether the wire
// even sees these.
// Forward only on non-default; per-backend cap-gates wire visibility.
...(params.skipSpecialTokens === false
? { skip_special_tokens: false }
: {}),

View file

@ -413,15 +413,11 @@ export function ChatSettingsPanel({
}: ChatSettingsPanelProps) {
const isMobile = useIsMobile();
const isGguf = useChatRuntimeStore((s) => s.activeGgufVariant) != null;
// For non-external (local) models we show every knob — providerCapabilities
// is only consulted when `isExternalModel` is true. An external model with an
// unknown provider falls back to the OpenAI-compat shape via
// getProviderCapabilities, so these flags never undercount support.
// GGUF (llama-server) honours frequency_penalty/seed/stop/parallel_tool_calls;
// the safetensors / HF transformers path does not, so we hide those toggles
// on local non-GGUF backends to keep the UI honest. Anything still set in
// params from a prior GGUF session is harmlessly ignored by the safetensors
// worker, so this is purely a presentation gate.
// Local models show every knob (providerCapabilities only consulted
// when isExternalModel; unknown providers fall back to OPENAI_COMPAT_BASE).
// GGUF llama-server honours frequency_penalty/seed/stop/parallel_tool_calls;
// HF transformers path doesn't, so hide on local non-GGUF (stale params
// are harmlessly ignored by the safetensors worker).
const localSamplerSupportsExtras = !isExternalModel ? isGguf : true;
const showTemperature =
!isExternalModel || Boolean(providerCapabilities?.temperature);
@ -446,9 +442,7 @@ export function ChatSettingsPanel({
const showParallelToolCalls = isExternalModel
? Boolean(providerCapabilities?.parallelToolCalls)
: localSamplerSupportsExtras;
// Extended llama.cpp / vLLM / OpenRouter samplers. Same gate as the
// core knobs above: external → providerCapabilities flag; local →
// GGUF-only (safetensors transformers ignores these).
// Extended samplers: external uses cap flag, local is GGUF-only.
const capAdv = (k: keyof ProviderCapabilities): boolean =>
isExternalModel
? Boolean(providerCapabilities?.[k])
@ -484,9 +478,8 @@ export function ChatSettingsPanel({
postSamplingProbs: capAdv("postSamplingProbs"),
};
const showAdvancedSamplingSection = Object.values(advCaps).some(Boolean);
// Per-provider stop cap from provider-capabilities.ts; backend
// re-trims on the wire if a stale UI sends more than the upstream
// accepts.
// Per-provider stop cap; backend re-trims on the wire if a stale
// UI sends more than the upstream accepts.
const stopMaxEntries = getProviderStopMax(externalProviderType);
const serviceTierOptions = getServiceTierOptions(externalProviderType);
const hasModelContent =
@ -1379,10 +1372,9 @@ export function ChatSettingsPanel({
Seed
</span>
<InfoHint>
Best-effort determinism seed. Same seed + prompt =
same output (approximately). OpenAI Chat Completions
and OpenAI-compat local backends honor it; OpenAI
Responses and Anthropic silently drop it.
Best-effort determinism. OpenAI Chat and OAI-compat
local backends honor it; OpenAI Responses and Anthropic
silently drop it.
</InfoHint>
</div>
<Input
@ -1413,11 +1405,9 @@ export function ChatSettingsPanel({
Stop sequences
</span>
<InfoHint>
Strings that halt generation as soon as the model
emits them. Enter a value and press Enter or comma
to commit a chip. Backend translates to
`stop_sequences` on Anthropic and `stop` on OpenAI
Chat (capped at 4 entries).
Strings that halt generation. Enter or comma to commit.
Maps to `stop_sequences` (Anthropic) / `stop` (OpenAI,
cap 4).
</InfoHint>
</div>
<StopSequencesInput
@ -1435,22 +1425,16 @@ export function ChatSettingsPanel({
Service tier
</span>
<InfoHint>
Provider routing tier. `auto` (default) lets the
provider choose. `flex` / `priority` / `scale` route
to higher-latency-tolerant or premium queues on
OpenAI; `standard_only` opts out of Anthropic's
priority tier.
Provider routing tier. `auto` = provider default.
OpenAI: flex / priority / scale. Anthropic:
`standard_only` opts out of Priority Tier.
</InfoHint>
</div>
<Select
value={params.serviceTier ?? "auto"}
onValueChange={(value) => {
// Store every selected tier verbatim, including the
// explicit "auto". On Anthropic the docs distinguish
// omitting service_tier (provider default) from
// setting "auto" (opt into Priority Tier when
// available); preserve the user's choice through to
// the adapter so the wire reflects it.
// Store "auto" verbatim: Anthropic distinguishes
// omitted (provider default) from auto (Priority Tier opt-in).
const allowed: readonly ServiceTier[] = serviceTierOptions;
if (allowed.includes(value as ServiceTier)) {
set("serviceTier")(value as ServiceTier);
@ -1480,11 +1464,8 @@ export function ChatSettingsPanel({
Parallel tool calls
</span>
<InfoHint>
When on, the model may dispatch multiple tool calls
in a single turn (default). Turn off to force one
tool call at a time. Anthropic implements this as
`disable_parallel_tool_use`; OpenAI as
`parallel_tool_calls`.
Allow multiple tool calls per turn (default).
Anthropic uses inverse `disable_parallel_tool_use`.
</InfoHint>
</div>
<Switch
@ -1623,7 +1604,7 @@ export function ChatSettingsPanel({
max={5}
step={0.1}
onChange={(v) => set("dynatempExponent")(v)}
info="llama.cpp `dynatemp_exponent`. Curve exponent for dynamic temperature. Paired with Dynatemp Range."
info="llama.cpp `dynatemp_exponent`. Curve exponent, pairs with Dynatemp Range."
/>
) : null}
{advCaps.mirostat ? (
@ -1699,7 +1680,7 @@ export function ChatSettingsPanel({
? "Off"
: undefined
}
info="llama.cpp DRY sampler. Master switch for the 4-field DRY chain (base / allowed length / penalty last N). 0 = off."
info="llama.cpp DRY master switch (unlocks base / allowed length / penalty last N). 0 = off."
/>
) : null}
{advCaps.dryBase && (params.dryMultiplier ?? 0) > 0 ? (
@ -1892,9 +1873,8 @@ export function ChatSettingsPanel({
Skip Special Tokens
</span>
<InfoHint>
vLLM `skip_special_tokens`. Default on. Turn off to keep
chat-template markers (e.g. `&lt;|im_end|&gt;`) in the
decoded output.
vLLM `skip_special_tokens` (default on). Off keeps
chat-template markers like `&lt;|im_end|&gt;` in output.
</InfoHint>
</div>
<Switch

View file

@ -1,13 +1,11 @@
// SPDX-License-Identifier: AGPL-3.0-only
// Copyright 2026-present the Unsloth AI Inc. team. All rights reserved. See /studio/LICENSE.AGPL-3.0
// Per-provider sampling capability matrix. Sourced from each
// provider's chat-completion docs (2026-05). The panel hides params
// the active provider does not accept so users never move a knob that
// would be silently dropped or rejected.
// When adding a new knob: default it to false on every SaaS bucket;
// only local backends + the permissive openrouter bucket should
// expose llama.cpp-specific samplers.
// Per-provider sampling capability matrix sourced from each provider's
// chat-completion docs (2026-05); panel hides params the active
// provider would silently drop or reject.
// New knobs: default to false on every SaaS bucket; only local +
// openrouter expose llama.cpp samplers.
export interface ProviderCapabilities {
/** OpenAI gpt-5.x / o-series reject via /v1/responses. */
temperature: boolean;
@ -76,17 +74,12 @@ export interface ProviderCapabilities {
}
/**
* Per-provider stop-sequence max count. Resolved by
* `getProviderStopMax(providerType)`. Mirrors the backend's
* `provider_info.stop_max` for the same provider type.
* - openai: 4 (Chat Completions hard cap; Responses drops stop)
* - anthropic: 16 (client-side guard; docs publish no max)
* - kimi: 5 (https://platform.kimi.ai/docs/api/chat)
* - deepseek: 16 (https://api-docs.deepseek.com/api/create-chat-completion)
* - mistral: 16 (no documented max; widen to permissive default)
* - gemini: 4 (https://ai.google.dev/gemini-api/docs/openai inherits OAI cap)
* - openrouter: 4 (normalises to OpenAI's chat schema)
* - default: 16 (covers ollama, vllm, llama.cpp, custom)
* Per-provider stop-sequence max count. Mirrors backend `stop_max`.
* openai 4 (Chat hard cap; Responses drops stop)
* anthropic 16 (client-side guard, docs no max)
* kimi 5 (https://platform.kimi.ai/docs/api/chat)
* deepseek 16 (https://api-docs.deepseek.com/api/create-chat-completion)
* mistral 16, gemini 4, openrouter 4, default 16 (ollama/vllm/llama.cpp/custom)
*/
const PROVIDER_STOP_MAX: Record<string, number> = {
openai: 4,
@ -114,13 +107,9 @@ export type ServiceTierOption =
| "standard_only";
/**
* Legal `service_tier` values per provider. Anthropic exposes only
* `auto` and `standard_only`. OpenAI in Studio is routed through
* `/v1/responses`, which the live docs list as
* `auto|default|flex|priority`; `scale` is excluded here even though
* the openai-python SDK type happens to include it. Other providers
* fall through to a permissive `auto` / `default` pair so the picker
* stays usable for OpenAI-compat backends.
* Legal `service_tier` per provider. anthropic=auto|standard_only;
* openai (/v1/responses)=auto|default|flex|priority (scale excluded
* though SDK lists it); others fall through to auto|default.
*/
export function getServiceTierOptions(
providerType: string | null | undefined,
@ -179,13 +168,12 @@ export function clampReasoningEffortToLevels(
*/
export const EXTERNAL_MAX_OUTPUT_TOKENS = 32768;
// Per-model max-output caps from each provider's docs (verified May 2026):
// Per-model max-output caps (verified May 2026). Longer prefixes first
// so .startsWith() picks the specific id over the family root.
// OpenAI: developers.openai.com/api/docs/models/<model>
// Anthropic: platform.claude.com/docs/en/about-claude/models/overview
// Gemini: ai.google.dev/gemini-api/docs/models
// DeepSeek: api-docs.deepseek.com/quick_start/pricing
// Order matters: list specific chat-class ids before broader gpt-5 /
// claude-opus-4 entries so the longer prefix wins via .startsWith().
const EXTERNAL_MAX_OUTPUT_TOKENS_BY_MODEL: Array<{
providerType: string;
prefixes: readonly string[];
@ -262,16 +250,13 @@ function _inferProviderFromOpenrouterId(
return null;
}
// Gates the composer's Search button. Backend translates
// enable_tools:["web_search"] into each provider's tool schema:
// OpenAI: tools:[{type:"web_search"}] on /v1/responses
// Anthropic: tools:[{type:"web_search_20250305", max_uses:5}] on /v1/messages
// OpenRouter: plugins:[{id:"web"}] (router's universal shape)
// Kimi: $web_search builtin (two-call round trip via
// _stream_kimi_web_search)
// Mistral excluded: their web_search is on the Agents API, not chat
// completions, and 400s if injected. Gemini grounded-search needs
// matching backend translation first.
// Gates the composer's Search button. Backend maps
// enable_tools:["web_search"] to each provider's tool schema:
// OpenAI: tools:[{type:"web_search"}] on /v1/responses
// Anthropic: tools:[{type:"web_search_20250305", max_uses:5}]
// OpenRouter: plugins:[{id:"web"}]
// Kimi: $web_search builtin (2-call via _stream_kimi_web_search)
// Mistral excluded (web_search lives on Agents API, 400s on /v1/chat).
export function providerSupportsBuiltinWebSearch(
providerType: string | null | undefined,
modelId?: string | null | undefined,
@ -418,9 +403,8 @@ export function providerSupportsBuiltinCodeExecution(
return false;
}
// OpenAI Responses-API image_generation tool. OpenAI cloud +
// Responses-family ids only; backend mirrors via is_openai_cloud so
// custom OAI-compat servers reporting provider_type="openai" don't 400.
// OpenAI Responses-API image_generation. OpenAI cloud +
// Responses-family ids only; backend mirrors via is_openai_cloud.
const OPENAI_IMAGE_GENERATION_MODEL_PREFIXES = [
"gpt-5.5-pro",
"gpt-5.5",
@ -510,10 +494,8 @@ function geminiImageModelAllowsGoogleSearch(modelId: string): boolean {
);
}
// Per-provider min on outbound max_tokens. Kimi thinking models need
// >=16000 or the response truncates mid-stream. Other providers fall
// through to the generic 64. chat-adapter bumps the user's stored
// maxTokens up to the floor on send; the slider min mirrors the same.
// Per-provider min on outbound max_tokens. Kimi thinking needs >=16000
// (truncates mid-stream below); chat-adapter bumps on send.
const EXTERNAL_MIN_OUTPUT_TOKENS_BY_PROVIDER: Record<string, number> = {
kimi: 16000,
};
@ -902,12 +884,9 @@ const PROVIDER_CAPABILITIES: Record<string, ProviderCapabilities> = {
postSamplingProbs: false,
},
mistral: OPENAI_COMPAT_BASE,
// Gemini's native generationConfig accepts temperature, topP, topK,
// presencePenalty, frequencyPenalty (not surfaced today), seed and
// stopSequences. minP and repetitionPenalty are not part of the
// contract -- see https://ai.google.dev/api/rest/v1beta/GenerationConfig.
// Backend request shaping lives in _stream_gemini in
// studio/backend/core/inference/external_provider.py.
// Gemini generationConfig: temperature/topP/topK/presencePenalty/
// frequencyPenalty/seed/stopSequences. No minP/repetitionPenalty.
// https://ai.google.dev/api/rest/v1beta/GenerationConfig
gemini: {
temperature: true,
topP: true,
@ -1053,14 +1032,9 @@ const PROVIDER_CAPABILITIES: Record<string, ProviderCapabilities> = {
const DEFAULT_EXTERNAL_CAPABILITIES = OPENAI_COMPAT_BASE;
// Per-model specialisations:
// openai + chat-class (gpt-4o, gpt-4-turbo, gpt-4, gpt-3.5):
// full sampling surface (OPENAI_CHAT_CAPABILITIES).
// openai + reasoning (gpt-5.x, o1, o3, o4): OPENAI_REASONING_CAPABILITIES.
// anthropic + claude-opus-4-7: strips temp/top_p/top_k (Opus only
// in 4.7; Sonnet/Haiku don't ship).
// deepseek reasoner: hides temp/top_p (silently ignored upstream).
// Returns null for local models (caller treats as "every knob applies").
// Per-model overrides: openai+chat-class -> OPENAI_CHAT_CAPABILITIES;
// anthropic claude-opus-4-7 strips temp/top_p/top_k; deepseek reasoner
// hides temp/top_p. Local (no providerType) returns null = every knob.
export function getProviderCapabilities(
providerType: string | null | undefined,
modelId?: string | null | undefined,
@ -1114,12 +1088,9 @@ const NO_REASONING_CAPS: ReasoningCaps = {
reasoningEffortLevels: DEFAULT_EFFORT_LEVELS,
};
// Order matters: longest prefixes first so find() picks the right
// bucket before the bare-family fallback ("claude-opus-4") sweeps.
// Levels per platform.claude.com/docs/en/about-claude/models/overview;
// 4.5 line uses budget_tokens mapped by the backend. Legacy 4.x
// (opus-4-1 / opus-4 / sonnet-4) supports Extended thinking per the
// overview table; sonnet-4 / opus-4 retire 2026-06-15.
// Longest prefixes first (find() must match before the bare-family
// fallback). Levels per platform.claude.com/docs/en/about-claude/models/overview.
// 4.5 line maps to budget_tokens; sonnet-4/opus-4 retire 2026-06-15.
const ANTHROPIC_REASONING_MODELS = [
{
prefixes: ["claude-opus-4-7"],
@ -1373,10 +1344,9 @@ function resolveGeminiReasoningCapabilities(
}
function resolveMistralReasoningCapabilities(modelId: string): ExternalReasoningCapabilities {
// magistral-* is native always-on (no reasoning_effort param; 422 if
// injected). mistral-{small,medium,vibe-cli}-latest is adjustable
// none/low/medium/high. See docs.mistral.ai/studio-api/conversations/
// reasoning + mistral.ai/news/magistral.
// magistral-*: native always-on (422 on reasoning_effort).
// mistral-{small,medium,vibe-cli}-latest: none/low/medium/high.
// https://docs.mistral.ai/studio-api/conversations/reasoning
if (
modelId === "magistral-medium-latest" ||
modelId === "magistral-small-latest"

View file

@ -320,11 +320,7 @@ export interface OpenAIChatCompletionsRequest {
seed?: number;
/** OAI Chat caps at 4; Anthropic mapped to `stop_sequences`. */
stop?: string[];
/**
* Per-provider enum (see getServiceTierOptions). Union stays
* permissive; external_provider.py drops values the active provider
* doesn't accept.
*/
/** Per-provider enum (see getServiceTierOptions); external_provider.py drops unsupported values. */
service_tier?:
| "auto"
| "default"

View file

@ -9,11 +9,8 @@ export type ServiceTier =
| "scale"
| "standard_only";
// All `number | null` / `boolean | null` fields below follow the same
// convention: `null` = field omitted from the wire request (provider
// uses its own default). Per-provider capability gating lives in
// provider-capabilities.ts; the chat-adapter forwards only when the
// active provider's bucket has the matching flag set true.
// null = field omitted from wire (provider default).
// Per-provider gates in provider-capabilities.ts.
export interface InferenceParams {
temperature: number;
topP: number;
@ -47,10 +44,7 @@ export interface InferenceParams {
mirostatEta: number | null;
/** OpenRouter `top_a`. Range [0, 1]. */
topA: number | null;
/**
* llama.cpp DRY sampler multiplier is the master switch (0 disables
* the 4-field chain). See llama.cpp/tools/server/README.md.
*/
/** llama.cpp DRY: multiplier is master switch (0 disables 4-field chain). See llama.cpp/tools/server/README.md. */
dryMultiplier: number | null;
/** Default 1.75. */
dryBase: number | null;

View file

@ -45,12 +45,9 @@ const NUMERIC_INFERENCE_FIELDS = [
"maxTokens",
] as const satisfies readonly (keyof PersistedInferenceParams)[];
// `seed` is numeric but nullable (null = no seed on the wire) so it
// can't go through the NUMERIC_INFERENCE_FIELDS finite-number filter.
// Keep this set in sync with `ServiceTier` in ../types/runtime.ts and
// `getServiceTierOptions` in ../provider-capabilities.ts. `scale` is
// kept in the allowlist for forward-compat with legacy persisted data
// even though the resolver no longer surfaces it for OpenAI Responses.
// `seed` is nullable so it skips NUMERIC_INFERENCE_FIELDS' finite-number filter.
// Keep in sync with ServiceTier (../types/runtime.ts) and getServiceTierOptions.
// "scale" stays for forward-compat with legacy persisted data.
const VALID_SERVICE_TIERS = new Set([
"auto",
"default",
@ -162,15 +159,12 @@ function sanitizeInferenceParams(
} else if (typeof value.seed === "number" && Number.isInteger(value.seed)) {
params.seed = value.seed;
}
// stop: cap at 16 (Anthropic's widest supported). The backend's
// per-provider stream helper re-truncates to the wire cap. An EMPTY
// array must persist (not be sanitized away) so clearing the last
// chip actually saves; otherwise the old stops come back on reload.
// stop: cap at 16 (Anthropic widest); backend re-truncates per provider.
// Empty array MUST persist or clearing the last chip is reverted on reload.
if (Array.isArray(value.stop)) {
const stops = value.stop.filter((s): s is string => typeof s === "string");
params.stop = stops.slice(0, 16);
}
// serviceTier: nullable enum string.
if (value.serviceTier === null) {
params.serviceTier = null;
} else if (
@ -182,8 +176,8 @@ function sanitizeInferenceParams(
if (typeof value.parallelToolCalls === "boolean") {
params.parallelToolCalls = value.parallelToolCalls;
}
// typicalP: nullable float (null = no typ_p on the wire, matching
// llama-server's default 1.0). Mirror seed's nullable-float handling.
// typicalP: nullable float (null = no typ_p on wire, matches
// llama-server default 1.0). Mirrors seed handling.
if (value.typicalP === null) {
params.typicalP = null;
} else if (
@ -192,8 +186,7 @@ function sanitizeInferenceParams(
) {
params.typicalP = value.typicalP;
}
// New llama.cpp / OpenRouter samplers — all nullable numbers with
// the same handling as `typicalP` / `seed`.
// Nullable numeric samplers (same handling as typicalP/seed).
for (const key of [
"topNSigma",
"repeatLastN",