Compare commits
62 commits
main
...
feat/expos
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
5ec1208206 | ||
|
|
1add4dbd0e | ||
|
|
0cf2097a81 | ||
|
|
6112ca4ecc | ||
|
|
80bf160b51 | ||
|
|
afed5fb791 | ||
|
|
319a95796c | ||
|
|
b60b0740c2 | ||
|
|
eaaf7142f6 | ||
|
|
64328962d0 | ||
|
|
3674e11f07 | ||
|
|
c22f6e48ff | ||
|
|
0234bef047 | ||
|
|
22111744a4 | ||
|
|
6b300699bf | ||
|
|
60085606a7 | ||
|
|
facdff9ad7 | ||
|
|
a02320aa7b | ||
|
|
c43c48a7e0 | ||
|
|
4f9125a5f9 | ||
|
|
643c0a88f1 | ||
|
|
5d4ddd6b37 | ||
|
|
f7c11d8a0a | ||
|
|
b961c6f2f7 | ||
|
|
07831d4c0c | ||
|
|
0be1e09421 | ||
|
|
5218a01d14 | ||
|
|
4c3be18d00 | ||
|
|
67e371934c | ||
|
|
737c5ad0e0 | ||
|
|
48df6a98c8 | ||
|
|
0c68f79ebd | ||
|
|
d3ae9142a5 | ||
|
|
9cd730130e | ||
|
|
aee1b7b9c1 | ||
|
|
ad36aaa71d | ||
|
|
d7a09d975b | ||
|
|
fbdd4e58e0 | ||
|
|
e0a9b1d76a | ||
|
|
1d1a205a19 | ||
|
|
10ade237cd | ||
|
|
1d3d7ef39c | ||
|
|
1cc52465f3 | ||
|
|
95e143545f | ||
|
|
f200bc20c0 | ||
|
|
b48d68f8bf | ||
|
|
30d6ce201e | ||
|
|
b8cef29b50 | ||
|
|
d8a4627355 | ||
|
|
fdf0be484e | ||
|
|
d6765fddce | ||
|
|
3ef64c2d65 | ||
|
|
d6b4c36e0a | ||
|
|
aa9d8e2180 | ||
|
|
5316c29588 | ||
|
|
febadebefa | ||
|
|
ffda6bbc71 | ||
|
|
eefe40a6bf | ||
|
|
093f465620 | ||
|
|
807165810f | ||
|
|
3cd3a64088 | ||
|
|
91d04741ff |
22 changed files with 4652 additions and 313 deletions
|
|
@ -404,12 +404,18 @@ class AnthropicPassthroughEmitter:
|
|||
streaming response back to Anthropic format without executing anything.
|
||||
"""
|
||||
|
||||
def __init__(self) -> None:
|
||||
def __init__(self, *, parallel_tool_calls: Optional[bool] = None) -> None:
|
||||
self.block_index: int = -1
|
||||
self._current_block_type: Optional[str] = None # "text" | "tool_use" | None
|
||||
self._tool_call_states: dict = {} # delta index -> {block_index, id, name}
|
||||
self._usage: dict = {}
|
||||
self._stop_reason: str = "end_turn"
|
||||
# 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
|
||||
|
||||
def start(self, message_id: str, model: str) -> list[str]:
|
||||
return [
|
||||
|
|
@ -470,6 +476,13 @@ class AnthropicPassthroughEmitter:
|
|||
tool_calls = delta.get("tool_calls") or []
|
||||
for tc in tool_calls:
|
||||
tc_idx = tc.get("index", 0)
|
||||
# Serial-tool-call gate: latch on the first index we see
|
||||
# and silently drop deltas for any other.
|
||||
if self._serial_tool_calls:
|
||||
if self._first_tool_call_idx is None:
|
||||
self._first_tool_call_idx = tc_idx
|
||||
if tc_idx != self._first_tool_call_idx:
|
||||
continue
|
||||
fn = tc.get("function") or {}
|
||||
if tc_idx not in self._tool_call_states:
|
||||
# New tool call — close prior block, open tool_use block
|
||||
|
|
|
|||
|
|
@ -29,30 +29,62 @@ import structlog
|
|||
logger = structlog.get_logger(__name__)
|
||||
|
||||
|
||||
# Claude 4.7 (Opus/Sonnet/Haiku) removed temperature, top_p, and top_k —
|
||||
# the API returns 400 "<param> is deprecated for this model" if any of
|
||||
# them is set to a non-default value. The "Sampling parameters removed"
|
||||
# section of the 4.7 release notes is the authoritative reference:
|
||||
# https://platform.claude.com/docs/en/about-claude/models/whats-new-claude-4-7
|
||||
# 3.x and 4.5/4.6 still accept all three; match the 4-7 line strictly so
|
||||
# the knobs keep working on earlier families. The trailing -4-7[-.]/EOL
|
||||
# anchor keeps future versions (e.g. claude-opus-5) unaffected.
|
||||
def _normalize_stop_for_provider(
|
||||
stop: Optional[Union[str, list[str]]],
|
||||
provider_info: dict[str, Any],
|
||||
) -> Optional[Union[str, list[str]]]:
|
||||
"""Apply per-provider stop_max / stop_max_bytes caps and dedup.
|
||||
|
||||
Returns None when nothing survives the filter so callers can omit
|
||||
the field. Single strings are returned verbatim when they fit.
|
||||
"""
|
||||
if not stop:
|
||||
return None
|
||||
|
||||
stop_max = int(provider_info.get("stop_max", 16))
|
||||
stop_max_bytes_raw = provider_info.get("stop_max_bytes")
|
||||
stop_max_bytes = int(stop_max_bytes_raw) if stop_max_bytes_raw is not None else None
|
||||
|
||||
def allowed(s: str) -> bool:
|
||||
if not s:
|
||||
return False
|
||||
if stop_max_bytes is not None and len(s.encode("utf-8")) > stop_max_bytes:
|
||||
logger.warning(
|
||||
"dropping stop sequence longer than %d bytes",
|
||||
stop_max_bytes,
|
||||
)
|
||||
return False
|
||||
return True
|
||||
|
||||
if isinstance(stop, str):
|
||||
return stop if allowed(stop) else None
|
||||
if isinstance(stop, list):
|
||||
sequences = list(
|
||||
dict.fromkeys(s for s in stop if isinstance(s, str) and allowed(s))
|
||||
)
|
||||
if len(sequences) > stop_max:
|
||||
logger.warning(
|
||||
"stop sequences truncated to %d entries (received %d)",
|
||||
stop_max,
|
||||
len(sequences),
|
||||
)
|
||||
sequences = sequences[:stop_max]
|
||||
return sequences or None
|
||||
return None
|
||||
|
||||
|
||||
# Opus 4.7 removed temperature/top_p/top_k (400s on any non-default).
|
||||
# Only Opus shipped in 4.7; 3.x and 4.5/4.6 still accept all three.
|
||||
# Trailing -4-7[-.]/EOL anchor keeps future families (claude-opus-5
|
||||
# etc) unaffected.
|
||||
# https://platform.claude.com/docs/en/about-claude/models/whats-new-claude-4-7
|
||||
def _is_openai_family_cloud(base_url: Optional[str]) -> bool:
|
||||
"""True iff ``base_url`` points at OpenAI cloud or Azure OpenAI Foundry.
|
||||
|
||||
Anchored to the URL host so an attacker can't bypass the gate with a
|
||||
path or subdomain like ``https://evil.com/api.openai.com/v1`` or
|
||||
``https://api.openai.com.attacker.com/v1`` (CodeQL py/incomplete-url-
|
||||
substring-sanitization). Used to scope cloud-only Responses-API
|
||||
extensions (prompt_cache_retention, context_management compaction,
|
||||
container shell tool) that 400 on non-cloud OpenAI-compatible
|
||||
servers (ollama / llama.cpp / vLLM).
|
||||
|
||||
Azure Foundry resources are scoped to
|
||||
``<resource-name>.openai.azure.com``; match any subdomain via an
|
||||
`endswith` on the lowercased hostname, with the leading dot so
|
||||
`openai.azure.com` itself doesn't slip through (there is no
|
||||
apex-hosted Azure Foundry endpoint).
|
||||
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
|
||||
|
|
@ -65,9 +97,7 @@ def _is_openai_family_cloud(base_url: Optional[str]) -> bool:
|
|||
return host == "api.openai.com" or host.endswith(".openai.azure.com")
|
||||
|
||||
|
||||
_ANTHROPIC_4_7_SAMPLING_REMOVED = re.compile(
|
||||
r"^claude-(?:opus|sonnet|haiku)-4-7(?:[-.]|$)"
|
||||
)
|
||||
_ANTHROPIC_4_7_SAMPLING_REMOVED = re.compile(r"^claude-opus-4-7(?:[-.]|$)")
|
||||
_OPENAI_REASONING_SUMMARY_UNSUPPORTED = re.compile(r"^o3(?:[-.]|$)")
|
||||
_OPENAI_REASONING_STATUSES = {"in_progress", "completed", "incomplete"}
|
||||
|
||||
|
|
@ -866,9 +896,42 @@ class ExternalProviderClient:
|
|||
anthropic_code_exec_container_id: Optional[str] = None,
|
||||
prompt_cache_ttl: Optional[str] = None,
|
||||
compaction_threshold: Optional[int] = None,
|
||||
frequency_penalty: Optional[float] = None,
|
||||
seed: Optional[int] = None,
|
||||
stop: Optional[Union[str, list[str]]] = None,
|
||||
service_tier: Optional[str] = None,
|
||||
parallel_tool_calls: Optional[bool] = None,
|
||||
tools: Optional[list[dict[str, Any]]] = None,
|
||||
tool_choice: Optional[Any] = None,
|
||||
fast_mode: Optional[bool] = None,
|
||||
typical_p: Optional[float] = None,
|
||||
top_n_sigma: Optional[float] = None,
|
||||
repeat_last_n: Optional[int] = None,
|
||||
dynatemp_range: Optional[float] = None,
|
||||
dynatemp_exponent: Optional[float] = None,
|
||||
mirostat: Optional[int] = None,
|
||||
mirostat_tau: Optional[float] = None,
|
||||
mirostat_eta: Optional[float] = None,
|
||||
top_a: Optional[float] = None,
|
||||
dry_multiplier: Optional[float] = None,
|
||||
dry_base: Optional[float] = None,
|
||||
dry_allowed_length: Optional[int] = None,
|
||||
dry_penalty_last_n: Optional[int] = None,
|
||||
xtc_probability: Optional[float] = None,
|
||||
xtc_threshold: Optional[float] = None,
|
||||
min_keep: Optional[int] = None,
|
||||
ignore_eos: Optional[bool] = None,
|
||||
min_tokens: Optional[int] = None,
|
||||
skip_special_tokens: Optional[bool] = None,
|
||||
spaces_between_special_tokens: Optional[bool] = None,
|
||||
include_stop_str_in_output: Optional[bool] = None,
|
||||
truncate_prompt_tokens: Optional[int] = None,
|
||||
n_keep: Optional[int] = None,
|
||||
n_probs: Optional[int] = None,
|
||||
cache_prompt: Optional[bool] = None,
|
||||
return_tokens: Optional[bool] = None,
|
||||
timings_per_token: Optional[bool] = None,
|
||||
post_sampling_probs: Optional[bool] = None,
|
||||
stream: bool = True,
|
||||
) -> AsyncGenerator[str, None]:
|
||||
"""
|
||||
|
|
@ -877,13 +940,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``) 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.
|
||||
|
|
@ -911,6 +975,7 @@ class ExternalProviderClient:
|
|||
reasoning_effort,
|
||||
tools,
|
||||
tool_choice,
|
||||
stop = stop,
|
||||
):
|
||||
yield line
|
||||
return
|
||||
|
|
@ -929,6 +994,9 @@ class ExternalProviderClient:
|
|||
prompt_cache_ttl,
|
||||
compaction_threshold,
|
||||
tool_choice,
|
||||
stop = stop,
|
||||
service_tier = service_tier,
|
||||
parallel_tool_calls = parallel_tool_calls,
|
||||
fast_mode = fast_mode,
|
||||
):
|
||||
yield line
|
||||
|
|
@ -954,6 +1022,8 @@ class ExternalProviderClient:
|
|||
compaction_threshold,
|
||||
tools,
|
||||
tool_choice,
|
||||
service_tier = service_tier,
|
||||
parallel_tool_calls = parallel_tool_calls,
|
||||
):
|
||||
yield line
|
||||
return
|
||||
|
|
@ -978,6 +1048,11 @@ class ExternalProviderClient:
|
|||
messages,
|
||||
model,
|
||||
max_tokens,
|
||||
frequency_penalty = frequency_penalty,
|
||||
seed = seed,
|
||||
stop = stop,
|
||||
parallel_tool_calls = parallel_tool_calls,
|
||||
presence_penalty = presence_penalty,
|
||||
):
|
||||
yield line
|
||||
return
|
||||
|
|
@ -997,14 +1072,99 @@ class ExternalProviderClient:
|
|||
else:
|
||||
body["max_tokens"] = max_tokens
|
||||
|
||||
# Drop fields the registry flags as unusable so reasoning-class
|
||||
# models with fixed defaults (Kimi k2.6 etc) don't 400 on pydantic
|
||||
# default values that the route layer still fills in.
|
||||
# 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 {}
|
||||
if frequency_penalty is not None:
|
||||
body["frequency_penalty"] = frequency_penalty
|
||||
if seed is not None:
|
||||
# Mistral renames `seed` to `random_seed` on /v1/chat/completions.
|
||||
seed_field = provider_info.get("seed_field", "seed")
|
||||
body[seed_field] = seed
|
||||
normalized_stop = _normalize_stop_for_provider(stop, provider_info)
|
||||
if normalized_stop:
|
||||
body["stop"] = normalized_stop
|
||||
# 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
|
||||
):
|
||||
body["service_tier"] = service_tier
|
||||
if parallel_tool_calls is not None:
|
||||
body["parallel_tool_calls"] = parallel_tool_calls
|
||||
|
||||
# Extended OAI-compat samplers (OpenRouter `top_a`, vLLM output
|
||||
# knobs, llama.cpp samplers on custom proxies). Each is gated `is
|
||||
# not None` so explicit 0/False reach the wire; `body_omit` below
|
||||
# strips fields the upstream rejects.
|
||||
if typical_p is not None:
|
||||
body["typical_p"] = typical_p
|
||||
if top_n_sigma is not None:
|
||||
body["top_n_sigma"] = top_n_sigma
|
||||
if repeat_last_n is not None:
|
||||
body["repeat_last_n"] = repeat_last_n
|
||||
if dynatemp_range is not None:
|
||||
body["dynatemp_range"] = dynatemp_range
|
||||
if dynatemp_exponent is not None:
|
||||
body["dynatemp_exponent"] = dynatemp_exponent
|
||||
if mirostat is not None:
|
||||
body["mirostat"] = mirostat
|
||||
if mirostat_tau is not None:
|
||||
body["mirostat_tau"] = mirostat_tau
|
||||
if mirostat_eta is not None:
|
||||
body["mirostat_eta"] = mirostat_eta
|
||||
if top_a is not None:
|
||||
body["top_a"] = top_a
|
||||
if dry_multiplier is not None:
|
||||
body["dry_multiplier"] = dry_multiplier
|
||||
if dry_base is not None:
|
||||
body["dry_base"] = dry_base
|
||||
if dry_allowed_length is not None:
|
||||
body["dry_allowed_length"] = dry_allowed_length
|
||||
if dry_penalty_last_n is not None:
|
||||
body["dry_penalty_last_n"] = dry_penalty_last_n
|
||||
if xtc_probability is not None:
|
||||
body["xtc_probability"] = xtc_probability
|
||||
if xtc_threshold is not None:
|
||||
body["xtc_threshold"] = xtc_threshold
|
||||
if min_keep is not None:
|
||||
body["min_keep"] = min_keep
|
||||
if ignore_eos is not None:
|
||||
body["ignore_eos"] = ignore_eos
|
||||
if min_tokens is not None:
|
||||
body["min_tokens"] = min_tokens
|
||||
if skip_special_tokens is not None:
|
||||
body["skip_special_tokens"] = skip_special_tokens
|
||||
if spaces_between_special_tokens is not None:
|
||||
body["spaces_between_special_tokens"] = spaces_between_special_tokens
|
||||
if include_stop_str_in_output is not None:
|
||||
body["include_stop_str_in_output"] = include_stop_str_in_output
|
||||
if truncate_prompt_tokens is not None:
|
||||
body["truncate_prompt_tokens"] = truncate_prompt_tokens
|
||||
if n_keep is not None:
|
||||
body["n_keep"] = n_keep
|
||||
if n_probs is not None:
|
||||
body["n_probs"] = n_probs
|
||||
if cache_prompt is not None:
|
||||
body["cache_prompt"] = cache_prompt
|
||||
if return_tokens is not None:
|
||||
body["return_tokens"] = return_tokens
|
||||
if timings_per_token is not None:
|
||||
body["timings_per_token"] = timings_per_token
|
||||
if post_sampling_probs is not None:
|
||||
body["post_sampling_probs"] = post_sampling_probs
|
||||
|
||||
# Drop body fields the provider's registry entry locks down
|
||||
# (e.g. Kimi k2.5/k2.6 only accept temperature=1, top_p=1).
|
||||
# Also pop the renamed seed field so `body_omit=("seed",)` on a
|
||||
# provider with `seed_field` rename still strips correctly.
|
||||
_seed_field = provider_info.get("seed_field", "seed")
|
||||
for field in provider_info.get("body_omit", ()):
|
||||
body.pop(field, None)
|
||||
if field == "seed" and _seed_field != "seed":
|
||||
body.pop(_seed_field, None)
|
||||
|
||||
# Kimi thinking is a top-level body field. kimi-k2-thinking is
|
||||
# always on (ignore the toggle); kimi-k2.6 defaults on, can be
|
||||
|
|
@ -1345,6 +1505,12 @@ class ExternalProviderClient:
|
|||
messages: list[dict[str, Any]],
|
||||
model: str,
|
||||
max_tokens: Optional[int],
|
||||
*,
|
||||
frequency_penalty: Optional[float] = None,
|
||||
seed: Optional[int] = None,
|
||||
stop: Optional[Union[str, list[str]]] = None,
|
||||
parallel_tool_calls: Optional[bool] = None,
|
||||
presence_penalty: Optional[float] = None,
|
||||
) -> AsyncGenerator[str, None]:
|
||||
"""
|
||||
Kimi $web_search round-trip.
|
||||
|
|
@ -1384,11 +1550,25 @@ class ExternalProviderClient:
|
|||
if max_tokens is not None:
|
||||
body["max_tokens"] = max_tokens
|
||||
|
||||
# Strip body fields the Kimi registry declares unusable
|
||||
# (temperature/top_p — see body_omit in providers.py).
|
||||
# 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 {}
|
||||
if presence_penalty is not None:
|
||||
body["presence_penalty"] = presence_penalty
|
||||
if frequency_penalty is not None:
|
||||
body["frequency_penalty"] = frequency_penalty
|
||||
if seed is not None:
|
||||
seed_field = provider_info.get("seed_field", "seed")
|
||||
body[seed_field] = seed
|
||||
normalized_stop = _normalize_stop_for_provider(stop, provider_info)
|
||||
if normalized_stop:
|
||||
body["stop"] = normalized_stop
|
||||
if parallel_tool_calls is not None:
|
||||
body["parallel_tool_calls"] = parallel_tool_calls
|
||||
|
||||
# Drop body fields the provider's registry entry locks down.
|
||||
for field in provider_info.get("body_omit", ()):
|
||||
body.pop(field, None)
|
||||
|
||||
|
|
@ -1736,6 +1916,9 @@ class ExternalProviderClient:
|
|||
compaction_threshold: Optional[int] = None,
|
||||
tool_choice: Optional[Any] = None,
|
||||
*,
|
||||
stop: Optional[Union[str, list[str]]] = None,
|
||||
service_tier: Optional[str] = None,
|
||||
parallel_tool_calls: Optional[bool] = None,
|
||||
fast_mode: Optional[bool] = None,
|
||||
) -> AsyncGenerator[str, None]:
|
||||
"""
|
||||
|
|
@ -2037,6 +2220,32 @@ class ExternalProviderClient:
|
|||
body["temperature"] = temperature
|
||||
if top_k is not None and top_k > 0 and not sampling_removed:
|
||||
body["top_k"] = top_k
|
||||
|
||||
# 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:
|
||||
# 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())
|
||||
)
|
||||
if len(sequences) > 16:
|
||||
logger.warning(
|
||||
"stop_sequences truncated to 16 entries "
|
||||
"(received %d, client-side guard ceiling)",
|
||||
len(sequences),
|
||||
)
|
||||
sequences = sequences[:16]
|
||||
if sequences:
|
||||
body["stop_sequences"] = sequences
|
||||
if service_tier in ("auto", "standard_only"):
|
||||
body["service_tier"] = service_tier
|
||||
# Anthropic only caches a prefix when at least one cache_control
|
||||
# marker is attached to it — the frontend defaults
|
||||
# enable_prompt_caching to True for Anthropic, so treat `None` the
|
||||
|
|
@ -2232,6 +2441,16 @@ class ExternalProviderClient:
|
|||
if anthropic_code_exec_container_id:
|
||||
body["container"] = anthropic_code_exec_container_id
|
||||
|
||||
# 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")
|
||||
if not isinstance(tc, dict):
|
||||
tc = {"type": "auto"}
|
||||
tc["disable_parallel_tool_use"] = True
|
||||
body["tool_choice"] = tc
|
||||
|
||||
# Server-side compaction (beta `compact-2026-01-12`). Clamps
|
||||
# below-min thresholds to 50K so the request doesn't 400.
|
||||
# https://platform.claude.com/docs/en/build-with-claude/compaction
|
||||
|
|
@ -3235,6 +3454,8 @@ class ExternalProviderClient:
|
|||
reasoning_effort: Optional[str] = None,
|
||||
tools: Optional[list[dict[str, Any]]] = None,
|
||||
tool_choice: Optional[Any] = None,
|
||||
*,
|
||||
stop: Optional[Union[str, list[str]]] = None,
|
||||
) -> AsyncGenerator[str, None]:
|
||||
"""
|
||||
Call Google's native Gemini API and translate its streaming
|
||||
|
|
@ -3928,6 +4149,14 @@ class ExternalProviderClient:
|
|||
"thinkingBudget": thinking_budget,
|
||||
}
|
||||
|
||||
# Gemini's generationConfig.stopSequences (max 5 per native docs).
|
||||
# https://ai.google.dev/api/generate-content#generationconfig
|
||||
if stop is not None:
|
||||
seqs = [stop] if isinstance(stop, str) else list(stop)
|
||||
seqs = [s for s in seqs if isinstance(s, str) and s][:5]
|
||||
if seqs:
|
||||
gen_config["stopSequences"] = seqs
|
||||
|
||||
if gen_config:
|
||||
body["generationConfig"] = gen_config
|
||||
|
||||
|
|
@ -4951,6 +5180,9 @@ class ExternalProviderClient:
|
|||
compaction_threshold: Optional[int] = None,
|
||||
tools: Optional[list[dict[str, Any]]] = None,
|
||||
tool_choice: Optional[Any] = None,
|
||||
*,
|
||||
service_tier: Optional[str] = None,
|
||||
parallel_tool_calls: Optional[bool] = None,
|
||||
) -> AsyncGenerator[str, None]:
|
||||
"""
|
||||
Call OpenAI's /v1/responses endpoint and translate its SSE stream back
|
||||
|
|
@ -5272,6 +5504,12 @@ class ExternalProviderClient:
|
|||
"input": input_items,
|
||||
"stream": 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:
|
||||
body["parallel_tool_calls"] = bool(parallel_tool_calls)
|
||||
if previous_response_id:
|
||||
body["previous_response_id"] = previous_response_id
|
||||
# `summary: "auto"` is what makes /v1/responses emit reasoning
|
||||
|
|
|
|||
|
|
@ -4247,6 +4247,36 @@ class LlamaCppBackend:
|
|||
enable_thinking: Optional[bool] = None,
|
||||
reasoning_effort: Optional[str] = None,
|
||||
preserve_thinking: Optional[bool] = None,
|
||||
frequency_penalty: Optional[float] = None,
|
||||
seed: Optional[int] = None,
|
||||
parallel_tool_calls: Optional[bool] = None,
|
||||
typical_p: Optional[float] = None,
|
||||
top_n_sigma: Optional[float] = None,
|
||||
repeat_last_n: Optional[int] = None,
|
||||
dynatemp_range: Optional[float] = None,
|
||||
dynatemp_exponent: Optional[float] = None,
|
||||
mirostat: Optional[int] = None,
|
||||
mirostat_tau: Optional[float] = None,
|
||||
mirostat_eta: Optional[float] = None,
|
||||
dry_multiplier: Optional[float] = None,
|
||||
dry_base: Optional[float] = None,
|
||||
dry_allowed_length: Optional[int] = None,
|
||||
dry_penalty_last_n: Optional[int] = None,
|
||||
xtc_probability: Optional[float] = None,
|
||||
xtc_threshold: Optional[float] = None,
|
||||
min_keep: Optional[int] = None,
|
||||
ignore_eos: Optional[bool] = None,
|
||||
min_tokens: Optional[int] = None,
|
||||
skip_special_tokens: Optional[bool] = None,
|
||||
spaces_between_special_tokens: Optional[bool] = None,
|
||||
include_stop_str_in_output: Optional[bool] = None,
|
||||
truncate_prompt_tokens: Optional[int] = None,
|
||||
n_keep: Optional[int] = None,
|
||||
n_probs: Optional[int] = None,
|
||||
cache_prompt: Optional[bool] = None,
|
||||
return_tokens: Optional[bool] = None,
|
||||
timings_per_token: Optional[bool] = None,
|
||||
post_sampling_probs: Optional[bool] = None,
|
||||
) -> Generator[str | dict, None, None]:
|
||||
"""
|
||||
Send a chat completion request to llama-server and stream tokens back.
|
||||
|
|
@ -4286,8 +4316,77 @@ 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 (mirrors
|
||||
# `_normalize_stop_for_provider`); stale `stop=["", "END"]` 400s.
|
||||
if stop:
|
||||
payload["stop"] = stop
|
||||
if isinstance(stop, str):
|
||||
payload["stop"] = stop
|
||||
elif isinstance(stop, list):
|
||||
_cleaned = [s for s in stop if isinstance(s, str) and s]
|
||||
if _cleaned:
|
||||
payload["stop"] = _cleaned
|
||||
# `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:
|
||||
payload["seed"] = seed
|
||||
if parallel_tool_calls is not None:
|
||||
payload["parallel_tool_calls"] = parallel_tool_calls
|
||||
if typical_p is not None:
|
||||
payload["typical_p"] = typical_p
|
||||
if top_n_sigma is not None:
|
||||
payload["top_n_sigma"] = top_n_sigma
|
||||
if repeat_last_n is not None:
|
||||
payload["repeat_last_n"] = repeat_last_n
|
||||
if dynatemp_range is not None:
|
||||
payload["dynatemp_range"] = dynatemp_range
|
||||
if dynatemp_exponent is not None:
|
||||
payload["dynatemp_exponent"] = dynatemp_exponent
|
||||
if mirostat is not None:
|
||||
payload["mirostat"] = mirostat
|
||||
if mirostat_tau is not None:
|
||||
payload["mirostat_tau"] = mirostat_tau
|
||||
if mirostat_eta is not None:
|
||||
payload["mirostat_eta"] = mirostat_eta
|
||||
if dry_multiplier is not None:
|
||||
payload["dry_multiplier"] = dry_multiplier
|
||||
if dry_base is not None:
|
||||
payload["dry_base"] = dry_base
|
||||
if dry_allowed_length is not None:
|
||||
payload["dry_allowed_length"] = dry_allowed_length
|
||||
if dry_penalty_last_n is not None:
|
||||
payload["dry_penalty_last_n"] = dry_penalty_last_n
|
||||
if xtc_probability is not None:
|
||||
payload["xtc_probability"] = xtc_probability
|
||||
if xtc_threshold is not None:
|
||||
payload["xtc_threshold"] = xtc_threshold
|
||||
if min_keep is not None:
|
||||
payload["min_keep"] = min_keep
|
||||
if ignore_eos is not None:
|
||||
payload["ignore_eos"] = ignore_eos
|
||||
if min_tokens is not None:
|
||||
payload["min_tokens"] = min_tokens
|
||||
if skip_special_tokens is not None:
|
||||
payload["skip_special_tokens"] = skip_special_tokens
|
||||
if spaces_between_special_tokens is not None:
|
||||
payload["spaces_between_special_tokens"] = spaces_between_special_tokens
|
||||
if include_stop_str_in_output is not None:
|
||||
payload["include_stop_str_in_output"] = include_stop_str_in_output
|
||||
if truncate_prompt_tokens is not None:
|
||||
payload["truncate_prompt_tokens"] = truncate_prompt_tokens
|
||||
if n_keep is not None:
|
||||
payload["n_keep"] = n_keep
|
||||
if n_probs is not None:
|
||||
payload["n_probs"] = n_probs
|
||||
if cache_prompt is not None:
|
||||
payload["cache_prompt"] = cache_prompt
|
||||
if return_tokens is not None:
|
||||
payload["return_tokens"] = return_tokens
|
||||
if timings_per_token is not None:
|
||||
payload["timings_per_token"] = timings_per_token
|
||||
if post_sampling_probs is not None:
|
||||
payload["post_sampling_probs"] = post_sampling_probs
|
||||
payload["stream_options"] = {"include_usage": True}
|
||||
|
||||
url = f"{self.base_url}/v1/chat/completions"
|
||||
|
|
@ -4430,6 +4529,36 @@ class LlamaCppBackend:
|
|||
auto_heal_tool_calls: bool = True,
|
||||
tool_call_timeout: int = 300,
|
||||
session_id: Optional[str] = None,
|
||||
frequency_penalty: Optional[float] = None,
|
||||
seed: Optional[int] = None,
|
||||
parallel_tool_calls: Optional[bool] = None,
|
||||
typical_p: Optional[float] = None,
|
||||
top_n_sigma: Optional[float] = None,
|
||||
repeat_last_n: Optional[int] = None,
|
||||
dynatemp_range: Optional[float] = None,
|
||||
dynatemp_exponent: Optional[float] = None,
|
||||
mirostat: Optional[int] = None,
|
||||
mirostat_tau: Optional[float] = None,
|
||||
mirostat_eta: Optional[float] = None,
|
||||
dry_multiplier: Optional[float] = None,
|
||||
dry_base: Optional[float] = None,
|
||||
dry_allowed_length: Optional[int] = None,
|
||||
dry_penalty_last_n: Optional[int] = None,
|
||||
xtc_probability: Optional[float] = None,
|
||||
xtc_threshold: Optional[float] = None,
|
||||
min_keep: Optional[int] = None,
|
||||
ignore_eos: Optional[bool] = None,
|
||||
min_tokens: Optional[int] = None,
|
||||
skip_special_tokens: Optional[bool] = None,
|
||||
spaces_between_special_tokens: Optional[bool] = None,
|
||||
include_stop_str_in_output: Optional[bool] = None,
|
||||
truncate_prompt_tokens: Optional[int] = None,
|
||||
n_keep: Optional[int] = None,
|
||||
n_probs: Optional[int] = None,
|
||||
cache_prompt: Optional[bool] = None,
|
||||
return_tokens: Optional[bool] = None,
|
||||
timings_per_token: Optional[bool] = None,
|
||||
post_sampling_probs: Optional[bool] = None,
|
||||
) -> Generator[dict, None, None]:
|
||||
"""
|
||||
Agentic loop: let the model call tools, execute them, and continue.
|
||||
|
|
@ -4512,8 +4641,75 @@ class LlamaCppBackend:
|
|||
else (self._effective_context_length or _DEFAULT_MAX_TOKENS_FLOOR)
|
||||
)
|
||||
payload["t_max_predict_ms"] = _DEFAULT_T_MAX_PREDICT_MS
|
||||
# Same empty-string filter as the standard payload builder.
|
||||
if stop:
|
||||
payload["stop"] = stop
|
||||
if isinstance(stop, str):
|
||||
payload["stop"] = stop
|
||||
elif isinstance(stop, list):
|
||||
_cleaned = [s for s in stop if isinstance(s, str) and s]
|
||||
if _cleaned:
|
||||
payload["stop"] = _cleaned
|
||||
# Optional sampling extensions; gated on `is not None`.
|
||||
if frequency_penalty is not None:
|
||||
payload["frequency_penalty"] = frequency_penalty
|
||||
if seed is not None:
|
||||
payload["seed"] = seed
|
||||
if parallel_tool_calls is not None:
|
||||
payload["parallel_tool_calls"] = parallel_tool_calls
|
||||
if typical_p is not None:
|
||||
payload["typical_p"] = typical_p
|
||||
if top_n_sigma is not None:
|
||||
payload["top_n_sigma"] = top_n_sigma
|
||||
if repeat_last_n is not None:
|
||||
payload["repeat_last_n"] = repeat_last_n
|
||||
if dynatemp_range is not None:
|
||||
payload["dynatemp_range"] = dynatemp_range
|
||||
if dynatemp_exponent is not None:
|
||||
payload["dynatemp_exponent"] = dynatemp_exponent
|
||||
if mirostat is not None:
|
||||
payload["mirostat"] = mirostat
|
||||
if mirostat_tau is not None:
|
||||
payload["mirostat_tau"] = mirostat_tau
|
||||
if mirostat_eta is not None:
|
||||
payload["mirostat_eta"] = mirostat_eta
|
||||
if dry_multiplier is not None:
|
||||
payload["dry_multiplier"] = dry_multiplier
|
||||
if dry_base is not None:
|
||||
payload["dry_base"] = dry_base
|
||||
if dry_allowed_length is not None:
|
||||
payload["dry_allowed_length"] = dry_allowed_length
|
||||
if dry_penalty_last_n is not None:
|
||||
payload["dry_penalty_last_n"] = dry_penalty_last_n
|
||||
if xtc_probability is not None:
|
||||
payload["xtc_probability"] = xtc_probability
|
||||
if xtc_threshold is not None:
|
||||
payload["xtc_threshold"] = xtc_threshold
|
||||
if min_keep is not None:
|
||||
payload["min_keep"] = min_keep
|
||||
if ignore_eos is not None:
|
||||
payload["ignore_eos"] = ignore_eos
|
||||
if min_tokens is not None:
|
||||
payload["min_tokens"] = min_tokens
|
||||
if skip_special_tokens is not None:
|
||||
payload["skip_special_tokens"] = skip_special_tokens
|
||||
if spaces_between_special_tokens is not None:
|
||||
payload["spaces_between_special_tokens"] = spaces_between_special_tokens
|
||||
if include_stop_str_in_output is not None:
|
||||
payload["include_stop_str_in_output"] = include_stop_str_in_output
|
||||
if truncate_prompt_tokens is not None:
|
||||
payload["truncate_prompt_tokens"] = truncate_prompt_tokens
|
||||
if n_keep is not None:
|
||||
payload["n_keep"] = n_keep
|
||||
if n_probs is not None:
|
||||
payload["n_probs"] = n_probs
|
||||
if cache_prompt is not None:
|
||||
payload["cache_prompt"] = cache_prompt
|
||||
if return_tokens is not None:
|
||||
payload["return_tokens"] = return_tokens
|
||||
if timings_per_token is not None:
|
||||
payload["timings_per_token"] = timings_per_token
|
||||
if post_sampling_probs is not None:
|
||||
payload["post_sampling_probs"] = post_sampling_probs
|
||||
|
||||
try:
|
||||
_auth_headers = (
|
||||
|
|
@ -4996,6 +5192,12 @@ class LlamaCppBackend:
|
|||
_accumulated_predicted_ms += _it.get("predicted_ms", 0)
|
||||
_accumulated_predicted_n += _it.get("predicted_n", 0)
|
||||
|
||||
# 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]
|
||||
|
||||
assistant_msg = {"role": "assistant", "content": content_text}
|
||||
if tool_calls:
|
||||
assistant_msg["tool_calls"] = tool_calls
|
||||
|
|
@ -5198,8 +5400,78 @@ class LlamaCppBackend:
|
|||
else (self._effective_context_length or _DEFAULT_MAX_TOKENS_FLOOR)
|
||||
)
|
||||
stream_payload["t_max_predict_ms"] = _DEFAULT_T_MAX_PREDICT_MS
|
||||
# Same empty-string filter as the standard / tool payload builder.
|
||||
if stop:
|
||||
stream_payload["stop"] = stop
|
||||
if isinstance(stop, str):
|
||||
stream_payload["stop"] = stop
|
||||
elif isinstance(stop, list):
|
||||
_cleaned = [s for s in stop if isinstance(s, str) and s]
|
||||
if _cleaned:
|
||||
stream_payload["stop"] = _cleaned
|
||||
# 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:
|
||||
stream_payload["seed"] = seed
|
||||
if parallel_tool_calls is not None:
|
||||
stream_payload["parallel_tool_calls"] = parallel_tool_calls
|
||||
if typical_p is not None:
|
||||
stream_payload["typical_p"] = typical_p
|
||||
if top_n_sigma is not None:
|
||||
stream_payload["top_n_sigma"] = top_n_sigma
|
||||
if repeat_last_n is not None:
|
||||
stream_payload["repeat_last_n"] = repeat_last_n
|
||||
if dynatemp_range is not None:
|
||||
stream_payload["dynatemp_range"] = dynatemp_range
|
||||
if dynatemp_exponent is not None:
|
||||
stream_payload["dynatemp_exponent"] = dynatemp_exponent
|
||||
if mirostat is not None:
|
||||
stream_payload["mirostat"] = mirostat
|
||||
if mirostat_tau is not None:
|
||||
stream_payload["mirostat_tau"] = mirostat_tau
|
||||
if mirostat_eta is not None:
|
||||
stream_payload["mirostat_eta"] = mirostat_eta
|
||||
if dry_multiplier is not None:
|
||||
stream_payload["dry_multiplier"] = dry_multiplier
|
||||
if dry_base is not None:
|
||||
stream_payload["dry_base"] = dry_base
|
||||
if dry_allowed_length is not None:
|
||||
stream_payload["dry_allowed_length"] = dry_allowed_length
|
||||
if dry_penalty_last_n is not None:
|
||||
stream_payload["dry_penalty_last_n"] = dry_penalty_last_n
|
||||
if xtc_probability is not None:
|
||||
stream_payload["xtc_probability"] = xtc_probability
|
||||
if xtc_threshold is not None:
|
||||
stream_payload["xtc_threshold"] = xtc_threshold
|
||||
if min_keep is not None:
|
||||
stream_payload["min_keep"] = min_keep
|
||||
if ignore_eos is not None:
|
||||
stream_payload["ignore_eos"] = ignore_eos
|
||||
if min_tokens is not None:
|
||||
stream_payload["min_tokens"] = min_tokens
|
||||
if skip_special_tokens is not None:
|
||||
stream_payload["skip_special_tokens"] = skip_special_tokens
|
||||
if spaces_between_special_tokens is not None:
|
||||
stream_payload["spaces_between_special_tokens"] = (
|
||||
spaces_between_special_tokens
|
||||
)
|
||||
if include_stop_str_in_output is not None:
|
||||
stream_payload["include_stop_str_in_output"] = include_stop_str_in_output
|
||||
if truncate_prompt_tokens is not None:
|
||||
stream_payload["truncate_prompt_tokens"] = truncate_prompt_tokens
|
||||
if n_keep is not None:
|
||||
stream_payload["n_keep"] = n_keep
|
||||
if n_probs is not None:
|
||||
stream_payload["n_probs"] = n_probs
|
||||
if cache_prompt is not None:
|
||||
stream_payload["cache_prompt"] = cache_prompt
|
||||
if return_tokens is not None:
|
||||
stream_payload["return_tokens"] = return_tokens
|
||||
if timings_per_token is not None:
|
||||
stream_payload["timings_per_token"] = timings_per_token
|
||||
if post_sampling_probs is not None:
|
||||
stream_payload["post_sampling_probs"] = post_sampling_probs
|
||||
stream_payload["stream_options"] = {"include_usage": True}
|
||||
|
||||
cumulative = ""
|
||||
|
|
|
|||
|
|
@ -839,6 +839,7 @@ class InferenceOrchestrator:
|
|||
tool_call_timeout: int = 300,
|
||||
session_id: Optional[str] = None,
|
||||
use_adapter: Optional[Union[bool, str]] = None,
|
||||
parallel_tool_calls: Optional[bool] = None,
|
||||
**_unused,
|
||||
):
|
||||
"""Run the safetensors agentic tool loop in this (parent)
|
||||
|
|
@ -895,6 +896,7 @@ class InferenceOrchestrator:
|
|||
max_tool_iterations = max_tool_iterations,
|
||||
tool_call_timeout = tool_call_timeout,
|
||||
session_id = session_id,
|
||||
parallel_tool_calls = parallel_tool_calls,
|
||||
)
|
||||
|
||||
def generate_with_adapter_control(
|
||||
|
|
|
|||
|
|
@ -137,11 +137,20 @@ PROVIDER_REGISTRY: dict[str, dict[str, Any]] = {
|
|||
r"gemini-pro-latest|gemini-flash-latest|gemini-flash-lite-latest"
|
||||
r")$"
|
||||
),
|
||||
# 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",
|
||||
# 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",
|
||||
"deepseek-chat",
|
||||
"deepseek-reasoner",
|
||||
],
|
||||
|
|
@ -150,7 +159,7 @@ PROVIDER_REGISTRY: dict[str, dict[str, Any]] = {
|
|||
"supports_tool_calling": True,
|
||||
"auth_header": "Authorization",
|
||||
"auth_prefix": "Bearer ",
|
||||
"notes": "OpenAI-compatible API. deepseek-chat = V3, deepseek-reasoner = R1 thinking mode.",
|
||||
"notes": "OpenAI-compatible API. deepseek-v4-pro / deepseek-v4-flash are the new canonical ids; deepseek-chat / deepseek-reasoner remain as legacy aliases until 2026-07-24.",
|
||||
},
|
||||
"mistral": {
|
||||
"display_name": "Mistral AI",
|
||||
|
|
@ -180,6 +189,12 @@ PROVIDER_REGISTRY: dict[str, dict[str, Any]] = {
|
|||
r"mistral-(?:large|medium|small|tiny)-latest|"
|
||||
r"mistral-vibe-cli-latest)$"
|
||||
),
|
||||
# Mistral renames OpenAI's `seed` to `random_seed` on
|
||||
# /v1/chat/completions. https://docs.mistral.ai/api/endpoint/chat
|
||||
"seed_field": "random_seed",
|
||||
# Mistral's docs publish no max but third-party shims cap at 4;
|
||||
# match OpenAI Chat's cap to avoid silent upstream truncation.
|
||||
"stop_max": 4,
|
||||
},
|
||||
"kimi": {
|
||||
"display_name": "Kimi",
|
||||
|
|
@ -203,11 +218,21 @@ 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"
|
||||
# (and the same shape for top_p). Strip both fields from the
|
||||
# outbound body so the server falls back to its required defaults.
|
||||
"body_omit": ("temperature", "top_p"),
|
||||
# k2.5/k2.6 are reasoning-class: API locks temperature=1, top_p=1,
|
||||
# frequency_penalty; presence_penalty / seed / parallel_tool_calls
|
||||
# are undocumented in the Kimi chat schema.
|
||||
"body_omit": (
|
||||
"temperature",
|
||||
"top_p",
|
||||
"frequency_penalty",
|
||||
"presence_penalty",
|
||||
"seed",
|
||||
"parallel_tool_calls",
|
||||
),
|
||||
# Kimi accepts at most 5 stop strings (each <= 32 bytes) per
|
||||
# https://platform.kimi.ai/docs/api/chat
|
||||
"stop_max": 5,
|
||||
"stop_max_bytes": 32,
|
||||
},
|
||||
"qwen": {
|
||||
"display_name": "Qwen",
|
||||
|
|
@ -354,6 +379,9 @@ PROVIDER_REGISTRY: dict[str, dict[str, Any]] = {
|
|||
},
|
||||
"notes": "Unified gateway to 300+ models across all major providers. HTTP-Referer and X-Title headers sent for attribution.",
|
||||
"model_list_mode": "curated",
|
||||
# OpenRouter normalises to OpenAI's chat schema and inherits
|
||||
# the 4-entry stop cap.
|
||||
"stop_max": 4,
|
||||
},
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -105,6 +105,7 @@ def run_safetensors_tool_loop(
|
|||
max_tool_iterations: int = 25,
|
||||
tool_call_timeout: int = 300,
|
||||
session_id: Optional[str] = None,
|
||||
parallel_tool_calls: Optional[bool] = None,
|
||||
) -> Generator[dict, None, None]:
|
||||
"""Drive an agentic tool loop on top of a cumulative-text generator.
|
||||
|
||||
|
|
@ -293,6 +294,12 @@ def run_safetensors_tool_loop(
|
|||
yield {"type": "status", "text": ""}
|
||||
return
|
||||
|
||||
# Mirror the GGUF agentic-loop cap: when the caller opted out
|
||||
# of parallel tool calls, execute at most one per assistant
|
||||
# turn even if the model parsed more.
|
||||
if parallel_tool_calls is False and tool_calls:
|
||||
tool_calls = tool_calls[:1]
|
||||
|
||||
assistant_msg: dict = {"role": "assistant", "content": content_text}
|
||||
if tool_calls:
|
||||
assistant_msg["tool_calls"] = tool_calls
|
||||
|
|
|
|||
|
|
@ -863,13 +863,187 @@ class ChatCompletionRequest(BaseModel):
|
|||
"to auto-create."
|
||||
),
|
||||
)
|
||||
frequency_penalty: Optional[float] = Field(
|
||||
None,
|
||||
ge = -2.0,
|
||||
le = 2.0,
|
||||
description = (
|
||||
"OpenAI Chat Completions frequency penalty (-2.0 to 2.0). "
|
||||
"Forwarded only on providers that accept it; Anthropic "
|
||||
"Messages and the OpenAI Responses family silently drop it."
|
||||
),
|
||||
)
|
||||
seed: Optional[int] = Field(
|
||||
None,
|
||||
description = (
|
||||
"Best-effort determinism seed. Forwarded to OpenAI Chat and "
|
||||
"OAI-compat local backends; dropped on Anthropic and OpenAI Responses."
|
||||
),
|
||||
)
|
||||
service_tier: Optional[
|
||||
Literal["auto", "default", "flex", "priority", "scale", "standard_only"]
|
||||
] = Field(
|
||||
None,
|
||||
description = (
|
||||
"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 = (
|
||||
"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(
|
||||
None,
|
||||
ge = 0.0,
|
||||
le = 1.0,
|
||||
description = "llama.cpp `typ_p`. 1.0 disables. Local only.",
|
||||
)
|
||||
top_n_sigma: Optional[float] = Field(
|
||||
None,
|
||||
description = "llama.cpp `top_n_sigma`. -1 disables. Local only.",
|
||||
)
|
||||
repeat_last_n: Optional[int] = Field(
|
||||
None,
|
||||
description = "llama.cpp `repeat_last_n`. 0 disables, -1 = ctx-size. Local only.",
|
||||
)
|
||||
dynatemp_range: Optional[float] = Field(
|
||||
None,
|
||||
ge = 0.0,
|
||||
description = "llama.cpp `dynatemp_range`. 0 disables. Local only.",
|
||||
)
|
||||
dynatemp_exponent: Optional[float] = Field(
|
||||
None,
|
||||
ge = 0.0,
|
||||
description = "llama.cpp `dynatemp_exponent`. Pairs with dynatemp_range. Local only.",
|
||||
)
|
||||
mirostat: Optional[int] = Field(
|
||||
None,
|
||||
ge = 0,
|
||||
le = 2,
|
||||
description = "llama.cpp `mirostat` (0=off, 1=Mirostat, 2=Mirostat 2.0). Local only.",
|
||||
)
|
||||
mirostat_tau: Optional[float] = Field(
|
||||
None,
|
||||
ge = 0.0,
|
||||
description = "llama.cpp `mirostat_tau`. Local only.",
|
||||
)
|
||||
mirostat_eta: Optional[float] = Field(
|
||||
None,
|
||||
ge = 0.0,
|
||||
description = "llama.cpp `mirostat_eta`. Local only.",
|
||||
)
|
||||
top_a: Optional[float] = Field(
|
||||
None,
|
||||
ge = 0.0,
|
||||
le = 1.0,
|
||||
description = (
|
||||
"OpenRouter `top_a`. OpenRouter-only. "
|
||||
"https://openrouter.ai/docs/api/reference/parameters"
|
||||
),
|
||||
)
|
||||
dry_multiplier: Optional[float] = Field(
|
||||
None,
|
||||
ge = 0.0,
|
||||
description = (
|
||||
"llama.cpp DRY multiplier. 0 disables the dry_base / "
|
||||
"dry_allowed_length / dry_penalty_last_n chain. Local only."
|
||||
),
|
||||
)
|
||||
dry_base: Optional[float] = Field(
|
||||
None,
|
||||
ge = 1.0,
|
||||
description = "llama.cpp DRY base. Default 1.75. Local only.",
|
||||
)
|
||||
dry_allowed_length: Optional[int] = Field(
|
||||
None,
|
||||
ge = 0,
|
||||
description = "llama.cpp DRY allowed-length. Default 2. Local only.",
|
||||
)
|
||||
dry_penalty_last_n: Optional[int] = Field(
|
||||
None,
|
||||
description = "llama.cpp DRY scan window. 0 disables, -1 = ctx-size. Local only.",
|
||||
)
|
||||
xtc_probability: Optional[float] = Field(
|
||||
None,
|
||||
ge = 0.0,
|
||||
le = 1.0,
|
||||
description = "llama.cpp XTC probability. 0 disables; pairs with xtc_threshold. Local only.",
|
||||
)
|
||||
xtc_threshold: Optional[float] = Field(
|
||||
None,
|
||||
ge = 0.0,
|
||||
le = 1.0,
|
||||
description = "llama.cpp XTC threshold. Default 0.1. Local only.",
|
||||
)
|
||||
min_keep: Optional[int] = Field(
|
||||
None,
|
||||
ge = 0,
|
||||
description = "llama.cpp `min_keep` (force min N past every filter). Local only.",
|
||||
)
|
||||
ignore_eos: Optional[bool] = Field(
|
||||
None,
|
||||
description = "Continue past EOS. llama.cpp + vLLM only.",
|
||||
)
|
||||
min_tokens: Optional[int] = Field(
|
||||
None,
|
||||
ge = 0,
|
||||
description = "Min output tokens before stop / EOS. llama.cpp + vLLM only.",
|
||||
)
|
||||
skip_special_tokens: Optional[bool] = Field(
|
||||
None,
|
||||
description = "vLLM `skip_special_tokens` (default true). vLLM only.",
|
||||
)
|
||||
spaces_between_special_tokens: Optional[bool] = Field(
|
||||
None,
|
||||
description = "vLLM `spaces_between_special_tokens` (default true). vLLM only.",
|
||||
)
|
||||
include_stop_str_in_output: Optional[bool] = Field(
|
||||
None,
|
||||
description = "vLLM `include_stop_str_in_output`. Useful for agentic tools. vLLM only.",
|
||||
)
|
||||
truncate_prompt_tokens: Optional[int] = Field(
|
||||
None,
|
||||
ge = 1,
|
||||
description = "vLLM `truncate_prompt_tokens` (left-truncate prompt). vLLM only.",
|
||||
)
|
||||
n_keep: Optional[int] = Field(
|
||||
None,
|
||||
description = "llama.cpp `n_keep`. 0 disables, -1 = keep all. Local only.",
|
||||
)
|
||||
n_probs: Optional[int] = Field(
|
||||
None,
|
||||
ge = 0,
|
||||
description = "llama.cpp `n_probs` (top-N token probs). 0 disables. Local only.",
|
||||
)
|
||||
cache_prompt: Optional[bool] = Field(
|
||||
None,
|
||||
description = "llama.cpp `cache_prompt` (default true upstream). Local only.",
|
||||
)
|
||||
return_tokens: Optional[bool] = Field(
|
||||
None,
|
||||
description = "llama.cpp `return_tokens` (debug). Local only.",
|
||||
)
|
||||
timings_per_token: Optional[bool] = Field(
|
||||
None,
|
||||
description = "llama.cpp `timings_per_token` (perf debug). Local only.",
|
||||
)
|
||||
post_sampling_probs: Optional[bool] = Field(
|
||||
None,
|
||||
description = "llama.cpp `post_sampling_probs` (sampler debug). Local only.",
|
||||
)
|
||||
fast_mode: Optional[bool] = Field(
|
||||
None,
|
||||
description = (
|
||||
"[x-unsloth] Anthropic fast-mode toggle. On Claude Opus 4.6 / "
|
||||
"4.7 adds the `fast-mode-2026-02-01` beta header and sends "
|
||||
"`speed: 'fast'` for higher OTPS at premium pricing. Silently "
|
||||
"ignored on every other model + provider. See "
|
||||
"[x-unsloth] Anthropic fast-mode on Opus 4.6 / 4.7. Adds the "
|
||||
"fast-mode-2026-02-01 beta header + speed:'fast' for higher "
|
||||
"OTPS at premium pricing. Silently dropped elsewhere. "
|
||||
"https://platform.claude.com/docs/en/build-with-claude/fast-mode"
|
||||
),
|
||||
)
|
||||
|
|
|
|||
|
|
@ -99,6 +99,9 @@ class ChatExportResponse(BaseModel):
|
|||
|
||||
|
||||
class ChatInferenceSettings(BaseModel):
|
||||
# extra="forbid" requires every persisted key to be listed. Keep
|
||||
# aligned with PERSISTED_INFERENCE_PARAM_KEYS in
|
||||
# studio/frontend/src/features/chat/stores/chat-runtime-store.ts.
|
||||
model_config = ConfigDict(extra = "forbid")
|
||||
|
||||
temperature: Optional[float] = None
|
||||
|
|
@ -107,10 +110,47 @@ class ChatInferenceSettings(BaseModel):
|
|||
minP: Optional[float] = None
|
||||
repetitionPenalty: Optional[float] = None
|
||||
presencePenalty: Optional[float] = None
|
||||
frequencyPenalty: Optional[float] = Field(default = None, ge = -2.0, le = 2.0)
|
||||
seed: Optional[int] = None
|
||||
stop: Optional[list[str]] = None
|
||||
serviceTier: Optional[
|
||||
Literal["auto", "default", "flex", "priority", "scale", "standard_only"]
|
||||
] = None
|
||||
parallelToolCalls: Optional[bool] = None
|
||||
maxSeqLength: Optional[float] = None
|
||||
maxTokens: Optional[float] = None
|
||||
systemPrompt: Optional[str] = None
|
||||
trustRemoteCode: Optional[bool] = None
|
||||
fastMode: Optional[bool] = None
|
||||
# Extended llama.cpp / vLLM / OpenRouter samplers exposed by PR #5711.
|
||||
typicalP: Optional[float] = None
|
||||
topNSigma: Optional[float] = None
|
||||
repeatLastN: Optional[int] = None
|
||||
dynatempRange: Optional[float] = None
|
||||
dynatempExponent: Optional[float] = None
|
||||
mirostat: Optional[int] = None
|
||||
mirostatTau: Optional[float] = None
|
||||
mirostatEta: Optional[float] = None
|
||||
topA: Optional[float] = None
|
||||
dryMultiplier: Optional[float] = None
|
||||
dryBase: Optional[float] = None
|
||||
dryAllowedLength: Optional[int] = None
|
||||
dryPenaltyLastN: Optional[int] = None
|
||||
xtcProbability: Optional[float] = None
|
||||
xtcThreshold: Optional[float] = None
|
||||
minKeep: Optional[int] = None
|
||||
ignoreEos: Optional[bool] = None
|
||||
minTokens: Optional[int] = None
|
||||
skipSpecialTokens: Optional[bool] = None
|
||||
spacesBetweenSpecialTokens: Optional[bool] = None
|
||||
includeStopStrInOutput: Optional[bool] = None
|
||||
truncatePromptTokens: Optional[int] = None
|
||||
nKeep: Optional[int] = None
|
||||
nProbs: Optional[int] = None
|
||||
cachePrompt: Optional[bool] = None
|
||||
returnTokens: Optional[bool] = None
|
||||
timingsPerToken: Optional[bool] = None
|
||||
postSamplingProbs: Optional[bool] = None
|
||||
|
||||
|
||||
class ChatPreset(BaseModel):
|
||||
|
|
|
|||
|
|
@ -239,6 +239,19 @@ router = APIRouter()
|
|||
studio_router = APIRouter()
|
||||
|
||||
|
||||
def _clean_local_stop_list(stop) -> Optional[list[str]]:
|
||||
"""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
|
||||
if isinstance(stop, list):
|
||||
cleaned = [s for s in stop if isinstance(s, str) and s]
|
||||
return cleaned or None
|
||||
return None
|
||||
|
||||
|
||||
def _detect_safetensors_features(backend, chat_template: Optional[str]) -> dict:
|
||||
"""Classify reasoning/tool capabilities via the GGUF classifier so
|
||||
flags match across backends. gpt-oss is overridden because Harmony
|
||||
|
|
@ -2147,9 +2160,42 @@ async def _proxy_to_external_provider(
|
|||
anthropic_code_exec_container_id = payload.anthropic_code_exec_container_id,
|
||||
prompt_cache_ttl = payload.prompt_cache_ttl,
|
||||
compaction_threshold = payload.compaction_threshold,
|
||||
frequency_penalty = payload.frequency_penalty,
|
||||
seed = payload.seed,
|
||||
stop = payload.stop,
|
||||
service_tier = payload.service_tier,
|
||||
parallel_tool_calls = payload.parallel_tool_calls,
|
||||
tools = payload.tools,
|
||||
tool_choice = payload.tool_choice,
|
||||
fast_mode = payload.fast_mode,
|
||||
typical_p = payload.typical_p,
|
||||
top_n_sigma = payload.top_n_sigma,
|
||||
repeat_last_n = payload.repeat_last_n,
|
||||
dynatemp_range = payload.dynatemp_range,
|
||||
dynatemp_exponent = payload.dynatemp_exponent,
|
||||
mirostat = payload.mirostat,
|
||||
mirostat_tau = payload.mirostat_tau,
|
||||
mirostat_eta = payload.mirostat_eta,
|
||||
top_a = payload.top_a,
|
||||
dry_multiplier = payload.dry_multiplier,
|
||||
dry_base = payload.dry_base,
|
||||
dry_allowed_length = payload.dry_allowed_length,
|
||||
dry_penalty_last_n = payload.dry_penalty_last_n,
|
||||
xtc_probability = payload.xtc_probability,
|
||||
xtc_threshold = payload.xtc_threshold,
|
||||
min_keep = payload.min_keep,
|
||||
ignore_eos = payload.ignore_eos,
|
||||
min_tokens = payload.min_tokens,
|
||||
skip_special_tokens = payload.skip_special_tokens,
|
||||
spaces_between_special_tokens = payload.spaces_between_special_tokens,
|
||||
include_stop_str_in_output = payload.include_stop_str_in_output,
|
||||
truncate_prompt_tokens = payload.truncate_prompt_tokens,
|
||||
n_keep = payload.n_keep,
|
||||
n_probs = payload.n_probs,
|
||||
cache_prompt = payload.cache_prompt,
|
||||
return_tokens = payload.return_tokens,
|
||||
timings_per_token = payload.timings_per_token,
|
||||
post_sampling_probs = payload.post_sampling_probs,
|
||||
stream = payload.stream,
|
||||
)
|
||||
try:
|
||||
|
|
@ -2776,6 +2822,7 @@ async def openai_chat_completions(
|
|||
max_tokens = payload.max_tokens,
|
||||
repetition_penalty = payload.repetition_penalty,
|
||||
presence_penalty = payload.presence_penalty,
|
||||
stop = _clean_local_stop_list(payload.stop),
|
||||
cancel_event = cancel_event,
|
||||
enable_thinking = payload.enable_thinking,
|
||||
reasoning_effort = payload.reasoning_effort,
|
||||
|
|
@ -2790,6 +2837,36 @@ async def openai_chat_completions(
|
|||
if payload.tool_call_timeout is not None
|
||||
else 300,
|
||||
session_id = payload.session_id,
|
||||
frequency_penalty = payload.frequency_penalty,
|
||||
seed = payload.seed,
|
||||
parallel_tool_calls = payload.parallel_tool_calls,
|
||||
typical_p = payload.typical_p,
|
||||
dry_multiplier = payload.dry_multiplier,
|
||||
dry_base = payload.dry_base,
|
||||
dry_allowed_length = payload.dry_allowed_length,
|
||||
dry_penalty_last_n = payload.dry_penalty_last_n,
|
||||
xtc_probability = payload.xtc_probability,
|
||||
xtc_threshold = payload.xtc_threshold,
|
||||
min_keep = payload.min_keep,
|
||||
ignore_eos = payload.ignore_eos,
|
||||
min_tokens = payload.min_tokens,
|
||||
skip_special_tokens = payload.skip_special_tokens,
|
||||
spaces_between_special_tokens = payload.spaces_between_special_tokens,
|
||||
include_stop_str_in_output = payload.include_stop_str_in_output,
|
||||
truncate_prompt_tokens = payload.truncate_prompt_tokens,
|
||||
n_keep = payload.n_keep,
|
||||
n_probs = payload.n_probs,
|
||||
cache_prompt = payload.cache_prompt,
|
||||
return_tokens = payload.return_tokens,
|
||||
timings_per_token = payload.timings_per_token,
|
||||
post_sampling_probs = payload.post_sampling_probs,
|
||||
top_n_sigma = payload.top_n_sigma,
|
||||
repeat_last_n = payload.repeat_last_n,
|
||||
dynatemp_range = payload.dynatemp_range,
|
||||
dynatemp_exponent = payload.dynatemp_exponent,
|
||||
mirostat = payload.mirostat,
|
||||
mirostat_tau = payload.mirostat_tau,
|
||||
mirostat_eta = payload.mirostat_eta,
|
||||
)
|
||||
|
||||
_tool_sentinel = object()
|
||||
|
|
@ -2955,10 +3032,41 @@ async def openai_chat_completions(
|
|||
max_tokens = payload.max_tokens,
|
||||
repetition_penalty = payload.repetition_penalty,
|
||||
presence_penalty = payload.presence_penalty,
|
||||
stop = _clean_local_stop_list(payload.stop),
|
||||
cancel_event = cancel_event,
|
||||
enable_thinking = payload.enable_thinking,
|
||||
reasoning_effort = payload.reasoning_effort,
|
||||
preserve_thinking = payload.preserve_thinking,
|
||||
frequency_penalty = payload.frequency_penalty,
|
||||
seed = payload.seed,
|
||||
parallel_tool_calls = payload.parallel_tool_calls,
|
||||
typical_p = payload.typical_p,
|
||||
dry_multiplier = payload.dry_multiplier,
|
||||
dry_base = payload.dry_base,
|
||||
dry_allowed_length = payload.dry_allowed_length,
|
||||
dry_penalty_last_n = payload.dry_penalty_last_n,
|
||||
xtc_probability = payload.xtc_probability,
|
||||
xtc_threshold = payload.xtc_threshold,
|
||||
min_keep = payload.min_keep,
|
||||
ignore_eos = payload.ignore_eos,
|
||||
min_tokens = payload.min_tokens,
|
||||
skip_special_tokens = payload.skip_special_tokens,
|
||||
spaces_between_special_tokens = payload.spaces_between_special_tokens,
|
||||
include_stop_str_in_output = payload.include_stop_str_in_output,
|
||||
truncate_prompt_tokens = payload.truncate_prompt_tokens,
|
||||
n_keep = payload.n_keep,
|
||||
n_probs = payload.n_probs,
|
||||
cache_prompt = payload.cache_prompt,
|
||||
return_tokens = payload.return_tokens,
|
||||
timings_per_token = payload.timings_per_token,
|
||||
post_sampling_probs = payload.post_sampling_probs,
|
||||
top_n_sigma = payload.top_n_sigma,
|
||||
repeat_last_n = payload.repeat_last_n,
|
||||
dynatemp_range = payload.dynatemp_range,
|
||||
dynatemp_exponent = payload.dynatemp_exponent,
|
||||
mirostat = payload.mirostat,
|
||||
mirostat_tau = payload.mirostat_tau,
|
||||
mirostat_eta = payload.mirostat_eta,
|
||||
)
|
||||
|
||||
_gguf_sentinel = object()
|
||||
|
|
@ -3298,6 +3406,7 @@ async def openai_chat_completions(
|
|||
else 300,
|
||||
session_id = payload.session_id,
|
||||
use_adapter = payload.use_adapter,
|
||||
parallel_tool_calls = payload.parallel_tool_calls,
|
||||
)
|
||||
|
||||
_sf_tool_sentinel = object()
|
||||
|
|
@ -4101,6 +4210,9 @@ 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
|
||||
# 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
|
||||
|
||||
chat_tools = _translate_responses_tools_to_chat(payload.tools)
|
||||
if chat_tools is not None:
|
||||
|
|
@ -4110,13 +4222,7 @@ def _build_chat_request(
|
|||
if chat_tool_choice is not None:
|
||||
chat_kwargs["tool_choice"] = chat_tool_choice
|
||||
|
||||
req = ChatCompletionRequest(**chat_kwargs)
|
||||
# `parallel_tool_calls` is not a first-class field on ChatCompletionRequest,
|
||||
# but the model allows extras and _build_openai_passthrough_body forwards
|
||||
# only explicitly-known fields. Llama-server does not currently implement
|
||||
# parallel_tool_calls semantics, so we accept-and-ignore it on the
|
||||
# Responses side to avoid breaking SDK clients that always send it.
|
||||
return req
|
||||
return ChatCompletionRequest(**chat_kwargs)
|
||||
|
||||
|
||||
def _chat_tool_calls_to_responses_output(tool_calls: list[dict]) -> list[dict]:
|
||||
|
|
@ -4167,6 +4273,10 @@ async def _responses_non_streaming(
|
|||
msg = choices[0].get("message", {}) or {}
|
||||
text = msg.get("content", "") or ""
|
||||
tool_calls = msg.get("tool_calls") or []
|
||||
# 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]
|
||||
|
||||
usage_data = body.get("usage", {})
|
||||
input_tokens = usage_data.get("prompt_tokens", 0)
|
||||
|
|
@ -4288,6 +4398,11 @@ 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
|
||||
# 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
|
||||
|
||||
def _snapshot_output() -> list[dict]:
|
||||
"""Snapshot of all completed output items for response.completed."""
|
||||
|
|
@ -4404,6 +4519,11 @@ async def _responses_stream(
|
|||
|
||||
for tc in delta.get("tool_calls") or []:
|
||||
idx = tc.get("index", 0)
|
||||
if serial_tool_calls:
|
||||
if first_serial_idx is None:
|
||||
first_serial_idx = idx
|
||||
if idx != first_serial_idx:
|
||||
continue
|
||||
st = tool_call_state.get(idx)
|
||||
fn = tc.get("function") or {}
|
||||
if st is None:
|
||||
|
|
@ -4773,6 +4893,15 @@ async def anthropic_messages(
|
|||
if openai_tool_choice is None:
|
||||
openai_tool_choice = "auto"
|
||||
|
||||
# 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")
|
||||
if isinstance(_disable, bool):
|
||||
anthropic_parallel_tool_calls = not _disable
|
||||
|
||||
cancel_event = threading.Event()
|
||||
|
||||
# ── Tool routing ──────────────────────────────────────────
|
||||
|
|
@ -4869,6 +4998,7 @@ async def anthropic_messages(
|
|||
repetition_penalty = repetition_penalty,
|
||||
presence_penalty = presence_penalty,
|
||||
tool_choice = openai_tool_choice,
|
||||
parallel_tool_calls = anthropic_parallel_tool_calls,
|
||||
session_id = payload.session_id,
|
||||
cancel_id = payload.cancel_id,
|
||||
)
|
||||
|
|
@ -4887,6 +5017,7 @@ async def anthropic_messages(
|
|||
repetition_penalty = repetition_penalty,
|
||||
presence_penalty = presence_penalty,
|
||||
tool_choice = openai_tool_choice,
|
||||
parallel_tool_calls = anthropic_parallel_tool_calls,
|
||||
)
|
||||
|
||||
if server_tools:
|
||||
|
|
@ -4978,6 +5109,7 @@ async def anthropic_messages(
|
|||
auto_heal_tool_calls = True,
|
||||
tool_call_timeout = 300,
|
||||
session_id = payload.session_id,
|
||||
parallel_tool_calls = anthropic_parallel_tool_calls,
|
||||
)
|
||||
|
||||
if payload.stream:
|
||||
|
|
@ -5229,6 +5361,36 @@ def _build_passthrough_payload(
|
|||
min_p = None,
|
||||
repetition_penalty = None,
|
||||
presence_penalty = None,
|
||||
frequency_penalty = None,
|
||||
seed = None,
|
||||
parallel_tool_calls = None,
|
||||
typical_p = None,
|
||||
top_n_sigma = None,
|
||||
repeat_last_n = None,
|
||||
dynatemp_range = None,
|
||||
dynatemp_exponent = None,
|
||||
mirostat = None,
|
||||
mirostat_tau = None,
|
||||
mirostat_eta = None,
|
||||
dry_multiplier = None,
|
||||
dry_base = None,
|
||||
dry_allowed_length = None,
|
||||
dry_penalty_last_n = None,
|
||||
xtc_probability = None,
|
||||
xtc_threshold = None,
|
||||
min_keep = None,
|
||||
ignore_eos = None,
|
||||
min_tokens = None,
|
||||
skip_special_tokens = None,
|
||||
spaces_between_special_tokens = None,
|
||||
include_stop_str_in_output = None,
|
||||
truncate_prompt_tokens = None,
|
||||
n_keep = None,
|
||||
n_probs = None,
|
||||
cache_prompt = None,
|
||||
return_tokens = None,
|
||||
timings_per_token = None,
|
||||
post_sampling_probs = None,
|
||||
tool_choice = "auto",
|
||||
response_format = None,
|
||||
chat_template_kwargs = None,
|
||||
|
|
@ -5251,8 +5413,16 @@ def _build_passthrough_payload(
|
|||
else (backend_ctx or _DEFAULT_MAX_TOKENS_FLOOR)
|
||||
)
|
||||
body["t_max_predict_ms"] = _DEFAULT_T_MAX_PREDICT_MS
|
||||
# Strip empty stop entries (mirrors `_normalize_stop_for_provider`);
|
||||
# stale `stop=["", "END"]` would 400 llama-server.
|
||||
if stop:
|
||||
body["stop"] = stop
|
||||
if isinstance(stop, str):
|
||||
if stop:
|
||||
body["stop"] = stop
|
||||
elif isinstance(stop, list):
|
||||
cleaned = [s for s in stop if isinstance(s, str) and s]
|
||||
if cleaned:
|
||||
body["stop"] = cleaned
|
||||
if min_p is not None:
|
||||
body["min_p"] = min_p
|
||||
if repetition_penalty is not None:
|
||||
|
|
@ -5260,16 +5430,76 @@ def _build_passthrough_payload(
|
|||
body["repeat_penalty"] = repetition_penalty
|
||||
if presence_penalty is not None:
|
||||
body["presence_penalty"] = presence_penalty
|
||||
# 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:
|
||||
body["seed"] = seed
|
||||
if parallel_tool_calls is not None:
|
||||
body["parallel_tool_calls"] = parallel_tool_calls
|
||||
if typical_p is not None:
|
||||
body["typical_p"] = typical_p
|
||||
if top_n_sigma is not None:
|
||||
body["top_n_sigma"] = top_n_sigma
|
||||
if repeat_last_n is not None:
|
||||
body["repeat_last_n"] = repeat_last_n
|
||||
if dynatemp_range is not None:
|
||||
body["dynatemp_range"] = dynatemp_range
|
||||
if dynatemp_exponent is not None:
|
||||
body["dynatemp_exponent"] = dynatemp_exponent
|
||||
if mirostat is not None:
|
||||
body["mirostat"] = mirostat
|
||||
if mirostat_tau is not None:
|
||||
body["mirostat_tau"] = mirostat_tau
|
||||
if mirostat_eta is not None:
|
||||
body["mirostat_eta"] = mirostat_eta
|
||||
if dry_multiplier is not None:
|
||||
body["dry_multiplier"] = dry_multiplier
|
||||
if dry_base is not None:
|
||||
body["dry_base"] = dry_base
|
||||
if dry_allowed_length is not None:
|
||||
body["dry_allowed_length"] = dry_allowed_length
|
||||
if dry_penalty_last_n is not None:
|
||||
body["dry_penalty_last_n"] = dry_penalty_last_n
|
||||
if xtc_probability is not None:
|
||||
body["xtc_probability"] = xtc_probability
|
||||
if xtc_threshold is not None:
|
||||
body["xtc_threshold"] = xtc_threshold
|
||||
if min_keep is not None:
|
||||
body["min_keep"] = min_keep
|
||||
if ignore_eos is not None:
|
||||
body["ignore_eos"] = ignore_eos
|
||||
if min_tokens is not None:
|
||||
body["min_tokens"] = min_tokens
|
||||
if skip_special_tokens is not None:
|
||||
body["skip_special_tokens"] = skip_special_tokens
|
||||
if spaces_between_special_tokens is not None:
|
||||
body["spaces_between_special_tokens"] = spaces_between_special_tokens
|
||||
if include_stop_str_in_output is not None:
|
||||
body["include_stop_str_in_output"] = include_stop_str_in_output
|
||||
if truncate_prompt_tokens is not None:
|
||||
body["truncate_prompt_tokens"] = truncate_prompt_tokens
|
||||
if n_keep is not None:
|
||||
body["n_keep"] = n_keep
|
||||
if n_probs is not None:
|
||||
body["n_probs"] = n_probs
|
||||
if cache_prompt is not None:
|
||||
body["cache_prompt"] = cache_prompt
|
||||
if return_tokens is not None:
|
||||
body["return_tokens"] = return_tokens
|
||||
if timings_per_token is not None:
|
||||
body["timings_per_token"] = timings_per_token
|
||||
if post_sampling_probs is not None:
|
||||
body["post_sampling_probs"] = post_sampling_probs
|
||||
if response_format is not None:
|
||||
# llama-server applies a GBNF grammar derived from the JSON schema
|
||||
# when response_format is present. Field is documented flat at the
|
||||
# request root (tools/server/README.md), which is also what the
|
||||
# OpenAI SDK produces by spreading extra_body into the body top.
|
||||
# llama-server applies a GBNF grammar from the JSON schema.
|
||||
# Field is documented flat at the request root.
|
||||
body["response_format"] = response_format
|
||||
if chat_template_kwargs is not None:
|
||||
# Propagate reasoning / template overrides (e.g. enable_thinking)
|
||||
# so llama-server renders the Jinja template in the mode the caller
|
||||
# asked for instead of whatever default the model was loaded with.
|
||||
# Reasoning / template overrides (e.g. enable_thinking) so
|
||||
# llama-server renders the Jinja template in the requested mode.
|
||||
body["chat_template_kwargs"] = chat_template_kwargs
|
||||
return body
|
||||
|
||||
|
|
@ -5291,6 +5521,7 @@ async def _anthropic_passthrough_stream(
|
|||
repetition_penalty = None,
|
||||
presence_penalty = None,
|
||||
tool_choice = "auto",
|
||||
parallel_tool_calls = None,
|
||||
session_id = None,
|
||||
cancel_id = None,
|
||||
):
|
||||
|
|
@ -5309,6 +5540,7 @@ async def _anthropic_passthrough_stream(
|
|||
min_p = min_p,
|
||||
repetition_penalty = repetition_penalty,
|
||||
presence_penalty = presence_penalty,
|
||||
parallel_tool_calls = parallel_tool_calls,
|
||||
tool_choice = tool_choice,
|
||||
backend_ctx = llama_backend.context_length,
|
||||
)
|
||||
|
|
@ -5319,7 +5551,9 @@ async def _anthropic_passthrough_stream(
|
|||
_tracker.__enter__()
|
||||
|
||||
async def _stream():
|
||||
emitter = AnthropicPassthroughEmitter()
|
||||
emitter = AnthropicPassthroughEmitter(
|
||||
parallel_tool_calls = parallel_tool_calls,
|
||||
)
|
||||
for line in emitter.start(message_id, model_name):
|
||||
yield line
|
||||
|
||||
|
|
@ -5443,6 +5677,7 @@ async def _anthropic_passthrough_non_streaming(
|
|||
repetition_penalty = None,
|
||||
presence_penalty = None,
|
||||
tool_choice = "auto",
|
||||
parallel_tool_calls = None,
|
||||
):
|
||||
"""Non-streaming client-side pass-through."""
|
||||
target_url = f"{llama_backend.base_url}/v1/chat/completions"
|
||||
|
|
@ -5458,6 +5693,7 @@ async def _anthropic_passthrough_non_streaming(
|
|||
min_p = min_p,
|
||||
repetition_penalty = repetition_penalty,
|
||||
presence_penalty = presence_penalty,
|
||||
parallel_tool_calls = parallel_tool_calls,
|
||||
tool_choice = tool_choice,
|
||||
backend_ctx = llama_backend.context_length,
|
||||
)
|
||||
|
|
@ -5484,6 +5720,10 @@ async def _anthropic_passthrough_non_streaming(
|
|||
content_blocks.append(AnthropicResponseTextBlock(text = text))
|
||||
|
||||
tool_calls = message.get("tool_calls") or []
|
||||
# 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:
|
||||
fn = tc.get("function") or {}
|
||||
try:
|
||||
|
|
@ -5786,6 +6026,36 @@ def _build_openai_passthrough_body(payload, backend_ctx = None) -> dict:
|
|||
min_p = payload.min_p,
|
||||
repetition_penalty = payload.repetition_penalty,
|
||||
presence_penalty = payload.presence_penalty,
|
||||
frequency_penalty = payload.frequency_penalty,
|
||||
seed = payload.seed,
|
||||
parallel_tool_calls = payload.parallel_tool_calls,
|
||||
typical_p = payload.typical_p,
|
||||
top_n_sigma = payload.top_n_sigma,
|
||||
repeat_last_n = payload.repeat_last_n,
|
||||
dynatemp_range = payload.dynatemp_range,
|
||||
dynatemp_exponent = payload.dynatemp_exponent,
|
||||
mirostat = payload.mirostat,
|
||||
mirostat_tau = payload.mirostat_tau,
|
||||
mirostat_eta = payload.mirostat_eta,
|
||||
dry_multiplier = payload.dry_multiplier,
|
||||
dry_base = payload.dry_base,
|
||||
dry_allowed_length = payload.dry_allowed_length,
|
||||
dry_penalty_last_n = payload.dry_penalty_last_n,
|
||||
xtc_probability = payload.xtc_probability,
|
||||
xtc_threshold = payload.xtc_threshold,
|
||||
min_keep = payload.min_keep,
|
||||
ignore_eos = payload.ignore_eos,
|
||||
min_tokens = payload.min_tokens,
|
||||
skip_special_tokens = payload.skip_special_tokens,
|
||||
spaces_between_special_tokens = payload.spaces_between_special_tokens,
|
||||
include_stop_str_in_output = payload.include_stop_str_in_output,
|
||||
truncate_prompt_tokens = payload.truncate_prompt_tokens,
|
||||
n_keep = payload.n_keep,
|
||||
n_probs = payload.n_probs,
|
||||
cache_prompt = payload.cache_prompt,
|
||||
return_tokens = payload.return_tokens,
|
||||
timings_per_token = payload.timings_per_token,
|
||||
post_sampling_probs = payload.post_sampling_probs,
|
||||
tool_choice = tool_choice,
|
||||
response_format = _extract_response_format(payload),
|
||||
chat_template_kwargs = tpl_kwargs,
|
||||
|
|
|
|||
|
|
@ -63,16 +63,18 @@ def test_cpu_thread_cap_is_opt_in(raw):
|
|||
|
||||
|
||||
# Anything that is not a positive integer raises a clear ValueError.
|
||||
@pytest.mark.parametrize("raw", ["zero", "0", "-3", "1.5", "abc", "8a", "0x4", "1e3", "4 0"])
|
||||
@pytest.mark.parametrize(
|
||||
"raw", ["zero", "0", "-3", "1.5", "abc", "8a", "0x4", "1e3", "4 0"]
|
||||
)
|
||||
def test_cpu_thread_cap_requires_positive_integer(raw):
|
||||
with pytest.raises(ValueError, match="must be a positive integer"):
|
||||
with pytest.raises(ValueError, match = "must be a positive integer"):
|
||||
configure_cpu_threads({"UNSLOTH_CPU_THREADS": raw})
|
||||
|
||||
|
||||
# env=None path uses real os.environ (production call from run.py / main.py).
|
||||
def test_cpu_thread_cap_uses_os_environ_when_env_is_none(monkeypatch):
|
||||
for variable in (*_THREAD_POOL_ENV_VARS, "UNSLOTH_CPU_THREADS"):
|
||||
monkeypatch.delenv(variable, raising=False)
|
||||
monkeypatch.delenv(variable, raising = False)
|
||||
monkeypatch.setenv("UNSLOTH_CPU_THREADS", "3")
|
||||
|
||||
configure_cpu_threads()
|
||||
|
|
@ -84,7 +86,7 @@ def test_cpu_thread_cap_uses_os_environ_when_env_is_none(monkeypatch):
|
|||
# Calling twice must not flip any seeded value.
|
||||
def test_cpu_thread_cap_idempotent(monkeypatch):
|
||||
for variable in (*_THREAD_POOL_ENV_VARS, "UNSLOTH_CPU_THREADS"):
|
||||
monkeypatch.delenv(variable, raising=False)
|
||||
monkeypatch.delenv(variable, raising = False)
|
||||
monkeypatch.setenv("UNSLOTH_CPU_THREADS", "5")
|
||||
|
||||
configure_cpu_threads()
|
||||
|
|
@ -138,9 +140,9 @@ def test_invalid_cpu_thread_cap_exits_without_traceback(entry_point):
|
|||
|
||||
result = subprocess.run(
|
||||
[sys.executable, str(entry_point)],
|
||||
env=env,
|
||||
capture_output=True,
|
||||
text=True,
|
||||
env = env,
|
||||
capture_output = True,
|
||||
text = True,
|
||||
)
|
||||
|
||||
assert result.returncode == 1
|
||||
|
|
|
|||
|
|
@ -275,18 +275,19 @@ class TestChatCompletionRequestToolFields:
|
|||
assert req.stop is None
|
||||
|
||||
def test_extra_fields_accepted(self):
|
||||
# `frequency_penalty`, `seed`, `response_format` are not yet
|
||||
# explicitly declared but must survive Pydantic parsing now that
|
||||
# extra="allow" is set.
|
||||
# ``response_format`` is still an undeclared OpenAI-side field;
|
||||
# it must survive Pydantic parsing because extra="allow" is set.
|
||||
# ``frequency_penalty`` and ``seed`` were promoted to explicit
|
||||
# ChatCompletionRequest fields in the sampling-params PR, so
|
||||
# they now ride the attribute path, not model_extra.
|
||||
req = self._make(
|
||||
frequency_penalty = 0.5,
|
||||
seed = 42,
|
||||
response_format = {"type": "json_object"},
|
||||
)
|
||||
# Extras land in model_extra
|
||||
assert req.frequency_penalty == 0.5
|
||||
assert req.seed == 42
|
||||
assert req.model_extra is not None
|
||||
assert req.model_extra.get("frequency_penalty") == 0.5
|
||||
assert req.model_extra.get("seed") == 42
|
||||
assert req.model_extra.get("response_format") == {"type": "json_object"}
|
||||
|
||||
def test_unsloth_extensions_still_work(self):
|
||||
|
|
|
|||
1430
studio/backend/tests/test_sampling_params_routing.py
Normal file
1430
studio/backend/tests/test_sampling_params_routing.py
Normal file
File diff suppressed because it is too large
Load diff
112
studio/frontend/src/components/ui/stop-sequences-input.tsx
Normal file
112
studio/frontend/src/components/ui/stop-sequences-input.tsx
Normal file
|
|
@ -0,0 +1,112 @@
|
|||
// SPDX-License-Identifier: AGPL-3.0-only
|
||||
// Copyright 2026-present the Unsloth AI Inc. team. All rights reserved. See /studio/LICENSE.AGPL-3.0
|
||||
|
||||
import { Badge } from "@/components/ui/badge";
|
||||
import { Input } from "@/components/ui/input";
|
||||
import { cn } from "@/lib/utils";
|
||||
import { XIcon } from "lucide-react";
|
||||
import { type KeyboardEvent, useState } from "react";
|
||||
|
||||
/** 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;
|
||||
maxEntries?: number;
|
||||
disabled?: boolean;
|
||||
placeholder?: string;
|
||||
className?: string;
|
||||
"aria-label"?: string;
|
||||
}
|
||||
|
||||
export function StopSequencesInput({
|
||||
value,
|
||||
onChange,
|
||||
maxEntries = 4,
|
||||
disabled,
|
||||
placeholder = "Add stop sequence",
|
||||
className,
|
||||
"aria-label": ariaLabel,
|
||||
}: StopSequencesInputProps) {
|
||||
const [draft, setDraft] = useState("");
|
||||
const atCap = value.length >= maxEntries;
|
||||
|
||||
function commitDraft() {
|
||||
// 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)) {
|
||||
setDraft("");
|
||||
return;
|
||||
}
|
||||
onChange([...value, draft]);
|
||||
setDraft("");
|
||||
}
|
||||
|
||||
function removeChip(index: number) {
|
||||
if (disabled) return;
|
||||
onChange(value.filter((_, i) => i !== index));
|
||||
}
|
||||
|
||||
function handleKeyDown(event: KeyboardEvent<HTMLInputElement>) {
|
||||
if (disabled) return;
|
||||
if (event.key === "Enter" || event.key === ",") {
|
||||
event.preventDefault();
|
||||
commitDraft();
|
||||
return;
|
||||
}
|
||||
if (event.key === "Backspace" && !draft && value.length > 0) {
|
||||
event.preventDefault();
|
||||
onChange(value.slice(0, -1));
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<div
|
||||
data-slot="stop-sequences-input"
|
||||
className={cn(
|
||||
"flex flex-wrap items-center gap-1.5 rounded-md border border-input bg-transparent px-2 py-1.5 text-sm",
|
||||
"focus-within:border-ring focus-within:ring-[1px] focus-within:ring-ring/40",
|
||||
disabled && "cursor-not-allowed opacity-60",
|
||||
className,
|
||||
)}
|
||||
aria-label={ariaLabel}
|
||||
>
|
||||
{value.map((entry, index) => (
|
||||
<Badge
|
||||
// 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"
|
||||
>
|
||||
<span className="font-mono">{entry}</span>
|
||||
{!disabled ? (
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => removeChip(index)}
|
||||
className="ml-0.5 rounded-full p-0.5 text-muted-foreground transition-colors hover:bg-muted hover:text-foreground"
|
||||
aria-label={`Remove stop sequence ${entry}`}
|
||||
>
|
||||
<XIcon className="size-3" />
|
||||
</button>
|
||||
) : null}
|
||||
</Badge>
|
||||
))}
|
||||
<Input
|
||||
value={draft}
|
||||
onChange={(event) => setDraft(event.target.value)}
|
||||
onKeyDown={handleKeyDown}
|
||||
onBlur={commitDraft}
|
||||
placeholder={atCap ? `Max ${maxEntries} stops` : placeholder}
|
||||
disabled={disabled || atCap}
|
||||
aria-label={ariaLabel || placeholder}
|
||||
className={cn(
|
||||
"h-6 min-w-[8ch] flex-1 border-0 bg-transparent p-0 text-sm shadow-none",
|
||||
"focus-visible:ring-0 focus-visible:border-0",
|
||||
)}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
|
@ -1806,6 +1806,7 @@ export function createOpenAIStreamAdapter(): ChatModelAdapter {
|
|||
);
|
||||
const externalCapabilities = getProviderCapabilities(
|
||||
externalProvider?.providerType,
|
||||
externalSelection?.modelId,
|
||||
);
|
||||
const externalReasoningCaps: ReturnType<
|
||||
typeof getExternalReasoningCapabilities
|
||||
|
|
@ -1990,6 +1991,166 @@ export function createOpenAIStreamAdapter(): ChatModelAdapter {
|
|||
...(externalCapabilities?.presencePenalty
|
||||
? { presence_penalty: params.presencePenalty }
|
||||
: {}),
|
||||
// Optional sampling extras. Per-provider gates live in
|
||||
// provider-capabilities.ts; backend drops unknown fields.
|
||||
...(externalCapabilities?.frequencyPenalty
|
||||
? { frequency_penalty: params.frequencyPenalty }
|
||||
: {}),
|
||||
...(externalCapabilities?.seed && params.seed !== null
|
||||
? { seed: params.seed }
|
||||
: {}),
|
||||
...(externalCapabilities?.stop && params.stop.length > 0
|
||||
? { stop: params.stop }
|
||||
: {}),
|
||||
...(externalCapabilities?.serviceTier && params.serviceTier
|
||||
? { service_tier: params.serviceTier }
|
||||
: {}),
|
||||
// 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: cap-flag plus
|
||||
// non-default value gates so only meaningful knobs hit wire.
|
||||
...(externalCapabilities?.typicalP &&
|
||||
params.typicalP !== null &&
|
||||
params.typicalP !== 1
|
||||
? { typical_p: params.typicalP }
|
||||
: {}),
|
||||
...(externalCapabilities?.topNSigma &&
|
||||
params.topNSigma !== null &&
|
||||
params.topNSigma !== -1
|
||||
? { top_n_sigma: params.topNSigma }
|
||||
: {}),
|
||||
...(externalCapabilities?.repeatLastN &&
|
||||
params.repeatLastN !== null
|
||||
? { repeat_last_n: params.repeatLastN }
|
||||
: {}),
|
||||
// Dynatemp: range>0 unlocks both fields.
|
||||
...(externalCapabilities?.dynatempRange &&
|
||||
params.dynatempRange !== null &&
|
||||
params.dynatempRange > 0
|
||||
? {
|
||||
dynatemp_range: params.dynatempRange,
|
||||
...(externalCapabilities?.dynatempExponent &&
|
||||
params.dynatempExponent !== null
|
||||
? { dynatemp_exponent: params.dynatempExponent }
|
||||
: {}),
|
||||
}
|
||||
: {}),
|
||||
// Mirostat: mode!=0 unlocks tau + eta.
|
||||
...(externalCapabilities?.mirostat &&
|
||||
params.mirostat !== null &&
|
||||
params.mirostat !== 0
|
||||
? {
|
||||
mirostat: params.mirostat,
|
||||
...(externalCapabilities?.mirostatTau &&
|
||||
params.mirostatTau !== null
|
||||
? { mirostat_tau: params.mirostatTau }
|
||||
: {}),
|
||||
...(externalCapabilities?.mirostatEta &&
|
||||
params.mirostatEta !== null
|
||||
? { mirostat_eta: params.mirostatEta }
|
||||
: {}),
|
||||
}
|
||||
: {}),
|
||||
...(externalCapabilities?.topA &&
|
||||
params.topA !== null &&
|
||||
params.topA > 0
|
||||
? { top_a: params.topA }
|
||||
: {}),
|
||||
// DRY: multiplier>0 unlocks the 4-field chain.
|
||||
...(externalCapabilities?.dryMultiplier &&
|
||||
params.dryMultiplier !== null &&
|
||||
params.dryMultiplier > 0
|
||||
? {
|
||||
dry_multiplier: params.dryMultiplier,
|
||||
...(externalCapabilities?.dryBase &&
|
||||
params.dryBase !== null
|
||||
? { dry_base: params.dryBase }
|
||||
: {}),
|
||||
...(externalCapabilities?.dryAllowedLength &&
|
||||
params.dryAllowedLength !== null
|
||||
? { dry_allowed_length: params.dryAllowedLength }
|
||||
: {}),
|
||||
...(externalCapabilities?.dryPenaltyLastN &&
|
||||
params.dryPenaltyLastN !== null
|
||||
? { dry_penalty_last_n: params.dryPenaltyLastN }
|
||||
: {}),
|
||||
}
|
||||
: {}),
|
||||
// XTC: probability>0 unlocks threshold.
|
||||
...(externalCapabilities?.xtcProbability &&
|
||||
params.xtcProbability !== null &&
|
||||
params.xtcProbability > 0
|
||||
? {
|
||||
xtc_probability: params.xtcProbability,
|
||||
...(externalCapabilities?.xtcThreshold &&
|
||||
params.xtcThreshold !== null
|
||||
? { xtc_threshold: params.xtcThreshold }
|
||||
: {}),
|
||||
}
|
||||
: {}),
|
||||
...(externalCapabilities?.minKeep &&
|
||||
params.minKeep !== null &&
|
||||
params.minKeep > 0
|
||||
? { min_keep: params.minKeep }
|
||||
: {}),
|
||||
...(externalCapabilities?.ignoreEos && params.ignoreEos === true
|
||||
? { ignore_eos: true }
|
||||
: {}),
|
||||
...(externalCapabilities?.minTokens &&
|
||||
params.minTokens !== null &&
|
||||
params.minTokens > 0
|
||||
? { min_tokens: params.minTokens }
|
||||
: {}),
|
||||
// vLLM output-shape: default true for skip/spaces, false
|
||||
// for include-stop. Forward only on user opt-out.
|
||||
...(externalCapabilities?.skipSpecialTokens &&
|
||||
params.skipSpecialTokens === false
|
||||
? { skip_special_tokens: false }
|
||||
: {}),
|
||||
...(externalCapabilities?.spacesBetweenSpecialTokens &&
|
||||
params.spacesBetweenSpecialTokens === false
|
||||
? { spaces_between_special_tokens: false }
|
||||
: {}),
|
||||
...(externalCapabilities?.includeStopStrInOutput &&
|
||||
params.includeStopStrInOutput === true
|
||||
? { include_stop_str_in_output: true }
|
||||
: {}),
|
||||
...(externalCapabilities?.truncatePromptTokens &&
|
||||
params.truncatePromptTokens !== null &&
|
||||
params.truncatePromptTokens > 0
|
||||
? { truncate_prompt_tokens: params.truncatePromptTokens }
|
||||
: {}),
|
||||
// n_keep accepts -1 (keep all), so the gate is != 0.
|
||||
...(externalCapabilities?.nKeep &&
|
||||
params.nKeep !== null &&
|
||||
params.nKeep !== 0
|
||||
? { n_keep: params.nKeep }
|
||||
: {}),
|
||||
...(externalCapabilities?.nProbs &&
|
||||
params.nProbs !== null &&
|
||||
params.nProbs > 0
|
||||
? { n_probs: params.nProbs }
|
||||
: {}),
|
||||
...(externalCapabilities?.cachePrompt &&
|
||||
params.cachePrompt === false
|
||||
? { cache_prompt: false }
|
||||
: {}),
|
||||
...(externalCapabilities?.returnTokens &&
|
||||
params.returnTokens === true
|
||||
? { return_tokens: true }
|
||||
: {}),
|
||||
...(externalCapabilities?.timingsPerToken &&
|
||||
params.timingsPerToken === true
|
||||
? { timings_per_token: true }
|
||||
: {}),
|
||||
...(externalCapabilities?.postSamplingProbs &&
|
||||
params.postSamplingProbs === true
|
||||
? { post_sampling_probs: true }
|
||||
: {}),
|
||||
// Compose the enabled_tools list from the active pills;
|
||||
// backend maps each name to the provider's tool schema.
|
||||
...(webSearchEnabledForThisTurn ||
|
||||
|
|
@ -2045,9 +2206,8 @@ export function createOpenAIStreamAdapter(): ChatModelAdapter {
|
|||
isPromptCacheTtl(externalProvider.promptCacheTtl)
|
||||
? { prompt_cache_ttl: externalProvider.promptCacheTtl }
|
||||
: {}),
|
||||
// Anthropic fast mode (Opus 4.6 / 4.7 only); backend
|
||||
// silently drops on unsupported models as a second
|
||||
// line of defence.
|
||||
// Fast mode (Anthropic Opus 4.6 / 4.7). Backend drops on
|
||||
// unsupported models as second defence.
|
||||
...(params.fastMode &&
|
||||
providerSupportsFastMode(
|
||||
externalProvider.providerType,
|
||||
|
|
@ -2080,6 +2240,107 @@ export function createOpenAIStreamAdapter(): ChatModelAdapter {
|
|||
min_p: params.minP,
|
||||
repetition_penalty: params.repetitionPenalty,
|
||||
presence_penalty: params.presencePenalty,
|
||||
// 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 }
|
||||
: {}),
|
||||
...(params.seed !== null ? { seed: params.seed } : {}),
|
||||
...(params.stop.length > 0 ? { stop: params.stop } : {}),
|
||||
...(params.typicalP !== null && params.typicalP !== 1
|
||||
? { typical_p: params.typicalP }
|
||||
: {}),
|
||||
...(params.topNSigma !== null && params.topNSigma !== -1
|
||||
? { top_n_sigma: params.topNSigma }
|
||||
: {}),
|
||||
...(params.repeatLastN !== null
|
||||
? { repeat_last_n: params.repeatLastN }
|
||||
: {}),
|
||||
...(params.dynatempRange !== null && params.dynatempRange > 0
|
||||
? {
|
||||
dynatemp_range: params.dynatempRange,
|
||||
...(params.dynatempExponent !== null
|
||||
? { dynatemp_exponent: params.dynatempExponent }
|
||||
: {}),
|
||||
}
|
||||
: {}),
|
||||
...(params.mirostat !== null && params.mirostat !== 0
|
||||
? {
|
||||
mirostat: params.mirostat,
|
||||
...(params.mirostatTau !== null
|
||||
? { mirostat_tau: params.mirostatTau }
|
||||
: {}),
|
||||
...(params.mirostatEta !== null
|
||||
? { mirostat_eta: params.mirostatEta }
|
||||
: {}),
|
||||
}
|
||||
: {}),
|
||||
// DRY: multiplier>0 unlocks the 4-field chain.
|
||||
...(params.dryMultiplier !== null && params.dryMultiplier > 0
|
||||
? {
|
||||
dry_multiplier: params.dryMultiplier,
|
||||
...(params.dryBase !== null
|
||||
? { dry_base: params.dryBase }
|
||||
: {}),
|
||||
...(params.dryAllowedLength !== null
|
||||
? { dry_allowed_length: params.dryAllowedLength }
|
||||
: {}),
|
||||
...(params.dryPenaltyLastN !== null
|
||||
? { dry_penalty_last_n: params.dryPenaltyLastN }
|
||||
: {}),
|
||||
}
|
||||
: {}),
|
||||
// XTC: probability>0 unlocks threshold.
|
||||
...(params.xtcProbability !== null && params.xtcProbability > 0
|
||||
? {
|
||||
xtc_probability: params.xtcProbability,
|
||||
...(params.xtcThreshold !== null
|
||||
? { xtc_threshold: params.xtcThreshold }
|
||||
: {}),
|
||||
}
|
||||
: {}),
|
||||
...(params.minKeep !== null && params.minKeep > 0
|
||||
? { min_keep: params.minKeep }
|
||||
: {}),
|
||||
...(params.ignoreEos === true ? { ignore_eos: true } : {}),
|
||||
...(params.minTokens !== null && params.minTokens > 0
|
||||
? { min_tokens: params.minTokens }
|
||||
: {}),
|
||||
// Forward only on non-default; per-backend cap-gates wire visibility.
|
||||
...(params.skipSpecialTokens === false
|
||||
? { skip_special_tokens: false }
|
||||
: {}),
|
||||
...(params.spacesBetweenSpecialTokens === false
|
||||
? { spaces_between_special_tokens: false }
|
||||
: {}),
|
||||
...(params.includeStopStrInOutput === true
|
||||
? { include_stop_str_in_output: true }
|
||||
: {}),
|
||||
...(params.truncatePromptTokens !== null &&
|
||||
params.truncatePromptTokens > 0
|
||||
? { truncate_prompt_tokens: params.truncatePromptTokens }
|
||||
: {}),
|
||||
...(params.nKeep !== null && params.nKeep !== 0
|
||||
? { n_keep: params.nKeep }
|
||||
: {}),
|
||||
...(params.nProbs !== null && params.nProbs > 0
|
||||
? { n_probs: params.nProbs }
|
||||
: {}),
|
||||
...(params.cachePrompt === false ? { cache_prompt: false } : {}),
|
||||
...(params.returnTokens === true ? { return_tokens: true } : {}),
|
||||
...(params.timingsPerToken === true
|
||||
? { timings_per_token: true }
|
||||
: {}),
|
||||
...(params.postSamplingProbs === true
|
||||
? { post_sampling_probs: true }
|
||||
: {}),
|
||||
// Forward only on explicit opt-out (default true on every
|
||||
// backend; default omit keeps wire-shape stable for users
|
||||
// who never opened the new settings panel).
|
||||
...(params.parallelToolCalls === false
|
||||
? { parallel_tool_calls: false }
|
||||
: {}),
|
||||
image_base64: imageBase64,
|
||||
audio_base64: audioBase64,
|
||||
cancel_id: cancelId,
|
||||
|
|
|
|||
|
|
@ -698,7 +698,10 @@ export function ChatPage(): ReactElement {
|
|||
const provider = externalProvidersForChat.find(
|
||||
(p) => p.id === selection.providerId,
|
||||
);
|
||||
const baseCapabilities = getProviderCapabilities(provider?.providerType);
|
||||
const baseCapabilities = getProviderCapabilities(
|
||||
provider?.providerType,
|
||||
selection.modelId,
|
||||
);
|
||||
if (!baseCapabilities) return baseCapabilities;
|
||||
const anthropicThinkingEnabled =
|
||||
provider?.providerType === "anthropic" &&
|
||||
|
|
|
|||
|
|
@ -87,13 +87,17 @@ import {
|
|||
type ProviderCapabilities,
|
||||
getExternalMaxOutputTokens,
|
||||
getExternalMinOutputTokens,
|
||||
getProviderStopMax,
|
||||
getServiceTierOptions,
|
||||
providerSupportsBuiltinCodeExecution,
|
||||
providerSupportsFastMode,
|
||||
} from "./provider-capabilities";
|
||||
import { StopSequencesInput } from "@/components/ui/stop-sequences-input";
|
||||
import { useChatRuntimeStore } from "./stores/chat-runtime-store";
|
||||
import { ChatMcpServersDialog } from "./chat-mcp-servers-dialog";
|
||||
import { listMcpServers } from "./api/mcp-servers-api";
|
||||
import type { InferenceParams } from "./types/runtime";
|
||||
import type { InferenceParams, ServiceTier } from "./types/runtime";
|
||||
import { Input } from "@/components/ui/input";
|
||||
|
||||
export { defaultInferenceParams, type Preset } from "./presets/preset-policy";
|
||||
export type { InferenceParams } from "./types/runtime";
|
||||
|
|
@ -129,21 +133,11 @@ export function InfoHint({ children }: { children: ReactNode }) {
|
|||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Editable numeric value display.
|
||||
*
|
||||
* Renders as a single <input> that *looks* like text by default —
|
||||
* transparent background, no border, no ring — and only shows a faint
|
||||
* surface tint on hover/focus to signal editability. When unfocused,
|
||||
* the input shows the formatted display string (`displayValue ?? value`,
|
||||
* so labels like "Off" / "Max" still render); on focus, it switches to
|
||||
* the raw numeric value, selects it, and accepts free text input.
|
||||
* Commit happens on blur or Enter; Escape reverts. The clamp-to-range
|
||||
* happens on commit so users can type intermediate values without the
|
||||
* input fighting them mid-keystroke. Single component shared by every
|
||||
* slider value and the Context Length input so the click-to-edit
|
||||
* affordance is consistent across the panel.
|
||||
*/
|
||||
/** Editable numeric value display: transparent text-like input that
|
||||
* shows formatted display on blur (so "Off"/"Max" labels render) and
|
||||
* switches to the raw number on focus. Commits on blur/Enter, reverts
|
||||
* on Escape, clamps on commit. Shared by every slider value + the
|
||||
* Context Length input. */
|
||||
function snapToStep(
|
||||
value: number,
|
||||
step: number,
|
||||
|
|
@ -407,10 +401,14 @@ export function ChatSettingsPanel({
|
|||
externalProviderType = null,
|
||||
onReloadModel,
|
||||
}: ChatSettingsPanelProps) {
|
||||
// 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.
|
||||
const isMobile = useIsMobile();
|
||||
const isGguf = useChatRuntimeStore((s) => s.activeGgufVariant) != null;
|
||||
// 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);
|
||||
const showTopP = !isExternalModel || Boolean(providerCapabilities?.topP);
|
||||
|
|
@ -420,8 +418,60 @@ export function ChatSettingsPanel({
|
|||
!isExternalModel || Boolean(providerCapabilities?.repetitionPenalty);
|
||||
const showPresencePenalty =
|
||||
!isExternalModel || Boolean(providerCapabilities?.presencePenalty);
|
||||
const isMobile = useIsMobile();
|
||||
const isGguf = useChatRuntimeStore((s) => s.activeGgufVariant) != null;
|
||||
const showFrequencyPenalty = isExternalModel
|
||||
? Boolean(providerCapabilities?.frequencyPenalty)
|
||||
: localSamplerSupportsExtras;
|
||||
const showSeed = isExternalModel
|
||||
? Boolean(providerCapabilities?.seed)
|
||||
: localSamplerSupportsExtras;
|
||||
const showStop = isExternalModel
|
||||
? Boolean(providerCapabilities?.stop)
|
||||
: localSamplerSupportsExtras;
|
||||
const showServiceTier =
|
||||
isExternalModel && Boolean(providerCapabilities?.serviceTier);
|
||||
const showParallelToolCalls = isExternalModel
|
||||
? Boolean(providerCapabilities?.parallelToolCalls)
|
||||
: localSamplerSupportsExtras;
|
||||
// Extended samplers: external uses cap flag, local is GGUF-only.
|
||||
const capAdv = (k: keyof ProviderCapabilities): boolean =>
|
||||
isExternalModel
|
||||
? Boolean(providerCapabilities?.[k])
|
||||
: localSamplerSupportsExtras;
|
||||
const advCaps = {
|
||||
typicalP: capAdv("typicalP"),
|
||||
topNSigma: capAdv("topNSigma"),
|
||||
repeatLastN: capAdv("repeatLastN"),
|
||||
dynatempRange: capAdv("dynatempRange"),
|
||||
dynatempExponent: capAdv("dynatempExponent"),
|
||||
mirostat: capAdv("mirostat"),
|
||||
mirostatTau: capAdv("mirostatTau"),
|
||||
mirostatEta: capAdv("mirostatEta"),
|
||||
topA: capAdv("topA"),
|
||||
dryMultiplier: capAdv("dryMultiplier"),
|
||||
dryBase: capAdv("dryBase"),
|
||||
dryAllowedLength: capAdv("dryAllowedLength"),
|
||||
dryPenaltyLastN: capAdv("dryPenaltyLastN"),
|
||||
xtcProbability: capAdv("xtcProbability"),
|
||||
xtcThreshold: capAdv("xtcThreshold"),
|
||||
minKeep: capAdv("minKeep"),
|
||||
ignoreEos: capAdv("ignoreEos"),
|
||||
minTokens: capAdv("minTokens"),
|
||||
skipSpecialTokens: capAdv("skipSpecialTokens"),
|
||||
spacesBetweenSpecialTokens: capAdv("spacesBetweenSpecialTokens"),
|
||||
includeStopStrInOutput: capAdv("includeStopStrInOutput"),
|
||||
truncatePromptTokens: capAdv("truncatePromptTokens"),
|
||||
nKeep: capAdv("nKeep"),
|
||||
nProbs: capAdv("nProbs"),
|
||||
cachePrompt: capAdv("cachePrompt"),
|
||||
returnTokens: capAdv("returnTokens"),
|
||||
timingsPerToken: capAdv("timingsPerToken"),
|
||||
postSamplingProbs: capAdv("postSamplingProbs"),
|
||||
};
|
||||
const showAdvancedSamplingSection = Object.values(advCaps).some(Boolean);
|
||||
// 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 =
|
||||
!isExternalModel && (isGguf || Boolean(params.checkpoint));
|
||||
const speculativeType = useChatRuntimeStore((s) => s.speculativeType);
|
||||
|
|
@ -1291,6 +1341,143 @@ export function ChatSettingsPanel({
|
|||
info="Penalizes any token that has already appeared at least once, encouraging the model to introduce new topics. 0 = off."
|
||||
/>
|
||||
) : null}
|
||||
{showFrequencyPenalty ? (
|
||||
<ParamSlider
|
||||
label="Frequency Penalty"
|
||||
value={params.frequencyPenalty}
|
||||
min={-2}
|
||||
max={2}
|
||||
step={0.1}
|
||||
onChange={set("frequencyPenalty")}
|
||||
displayValue={
|
||||
params.frequencyPenalty === 0 ? "Off" : undefined
|
||||
}
|
||||
info="Down-weights tokens proportionally to how often they have already appeared. Negative values encourage repetition. 0 = off. OpenAI Chat Completions only; Anthropic and the OpenAI Responses family ignore it."
|
||||
/>
|
||||
) : null}
|
||||
{showSeed ? (
|
||||
<div className="flex items-center justify-between gap-3">
|
||||
<div className="flex min-w-0 items-center gap-1.5">
|
||||
<span className="min-w-0 text-[13px] font-medium leading-[1.25] tracking-nav text-nav-fg">
|
||||
Seed
|
||||
</span>
|
||||
<InfoHint>
|
||||
Best-effort determinism. OpenAI Chat and OAI-compat
|
||||
local backends honor it; OpenAI Responses and Anthropic
|
||||
silently drop it.
|
||||
</InfoHint>
|
||||
</div>
|
||||
<Input
|
||||
type="number"
|
||||
inputMode="numeric"
|
||||
value={params.seed ?? ""}
|
||||
placeholder="Random"
|
||||
onChange={(event) => {
|
||||
const raw = event.target.value;
|
||||
if (raw === "") {
|
||||
set("seed")(null);
|
||||
return;
|
||||
}
|
||||
const parsed = Number.parseInt(raw, 10);
|
||||
if (Number.isFinite(parsed)) {
|
||||
set("seed")(parsed);
|
||||
}
|
||||
}}
|
||||
className="h-8 w-[124px] shrink-0 text-right font-mono text-xs"
|
||||
aria-label="Seed"
|
||||
/>
|
||||
</div>
|
||||
) : null}
|
||||
{showStop ? (
|
||||
<div className="flex flex-col gap-1.5">
|
||||
<div className="flex min-w-0 items-center gap-1.5">
|
||||
<span className="min-w-0 text-[13px] font-medium leading-[1.25] tracking-nav text-nav-fg">
|
||||
Stop sequences
|
||||
</span>
|
||||
<InfoHint>
|
||||
Strings that halt generation. Enter or comma to commit.
|
||||
Maps to `stop_sequences` (Anthropic) / `stop` (OpenAI,
|
||||
cap 4).
|
||||
</InfoHint>
|
||||
</div>
|
||||
<StopSequencesInput
|
||||
value={params.stop}
|
||||
onChange={set("stop")}
|
||||
maxEntries={stopMaxEntries}
|
||||
aria-label="Stop sequences"
|
||||
/>
|
||||
</div>
|
||||
) : null}
|
||||
{showServiceTier ? (
|
||||
<div className="flex items-center justify-between gap-3">
|
||||
<div className="flex min-w-0 items-center gap-1.5">
|
||||
<span className="min-w-0 text-[13px] font-medium leading-[1.25] tracking-nav text-nav-fg">
|
||||
Service tier
|
||||
</span>
|
||||
<InfoHint>
|
||||
Provider routing tier. `auto` = provider default.
|
||||
OpenAI: flex / priority / scale. Anthropic:
|
||||
`standard_only` opts out of Priority Tier.
|
||||
</InfoHint>
|
||||
</div>
|
||||
<Select
|
||||
value={
|
||||
// Fall back to "auto" when the persisted tier is not
|
||||
// legal for the active provider (e.g. "priority" saved
|
||||
// on OpenAI, then user switched to Anthropic which only
|
||||
// accepts auto|standard_only). Without this Radix Select
|
||||
// shows a blank trigger.
|
||||
params.serviceTier &&
|
||||
(serviceTierOptions as readonly ServiceTier[]).includes(
|
||||
params.serviceTier,
|
||||
)
|
||||
? params.serviceTier
|
||||
: "auto"
|
||||
}
|
||||
onValueChange={(value) => {
|
||||
// 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);
|
||||
}
|
||||
}}
|
||||
>
|
||||
<SelectTrigger
|
||||
className="panel-select-trigger h-8 w-[140px] shrink-0"
|
||||
aria-label="Service tier"
|
||||
>
|
||||
<SelectValue />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
{serviceTierOptions.map((option) => (
|
||||
<SelectItem key={option} value={option}>
|
||||
{option}
|
||||
</SelectItem>
|
||||
))}
|
||||
</SelectContent>
|
||||
</Select>
|
||||
</div>
|
||||
) : null}
|
||||
{showParallelToolCalls ? (
|
||||
<div className="flex items-center justify-between gap-3">
|
||||
<div className="flex min-w-0 items-center gap-1.5">
|
||||
<span className="min-w-0 text-[13px] font-medium leading-[1.25] tracking-nav text-nav-fg">
|
||||
Parallel tool calls
|
||||
</span>
|
||||
<InfoHint>
|
||||
Allow multiple tool calls per turn (default).
|
||||
Anthropic uses inverse `disable_parallel_tool_use`.
|
||||
</InfoHint>
|
||||
</div>
|
||||
<Switch
|
||||
className="panel-switch shrink-0"
|
||||
checked={params.parallelToolCalls}
|
||||
onCheckedChange={set("parallelToolCalls")}
|
||||
aria-label="Parallel tool calls"
|
||||
/>
|
||||
</div>
|
||||
) : null}
|
||||
{!isExternalModel && !isGguf && (
|
||||
<ParamSlider
|
||||
label="Max Seq Length"
|
||||
|
|
@ -1299,7 +1486,7 @@ export function ChatSettingsPanel({
|
|||
max={32768}
|
||||
step={128}
|
||||
onChange={set("maxSeqLength")}
|
||||
info="Maximum context window size in tokens — input prompt plus generated output combined. Capped by the model's trained limit."
|
||||
info="Maximum context window in tokens (prompt plus generated output). Capped by the model's trained limit."
|
||||
/>
|
||||
)}
|
||||
<ParamSlider
|
||||
|
|
@ -1334,6 +1521,503 @@ export function ChatSettingsPanel({
|
|||
</div>
|
||||
</CollapsibleSection>
|
||||
|
||||
{showAdvancedSamplingSection ? (
|
||||
<CollapsibleSection label="Advanced Sampling" defaultOpen={false}>
|
||||
<div className="flex flex-col gap-5 pt-1">
|
||||
{advCaps.typicalP ? (
|
||||
<ParamSlider
|
||||
label="Typical P"
|
||||
value={params.typicalP ?? 1}
|
||||
min={0}
|
||||
max={1}
|
||||
step={0.05}
|
||||
onChange={(v) =>
|
||||
set("typicalP")(v >= 1 ? null : v)
|
||||
}
|
||||
displayValue={
|
||||
params.typicalP == null || params.typicalP >= 1
|
||||
? "Off"
|
||||
: undefined
|
||||
}
|
||||
info="llama.cpp `typ_p`. Locally typical sampling. 1.0 = off."
|
||||
/>
|
||||
) : null}
|
||||
{advCaps.topNSigma ? (
|
||||
<ParamSlider
|
||||
label="Top N Sigma"
|
||||
value={params.topNSigma ?? -1}
|
||||
min={-1}
|
||||
max={5}
|
||||
step={0.1}
|
||||
onChange={(v) =>
|
||||
set("topNSigma")(v <= -1 ? null : v)
|
||||
}
|
||||
displayValue={
|
||||
params.topNSigma == null || params.topNSigma <= -1
|
||||
? "Off"
|
||||
: undefined
|
||||
}
|
||||
info="llama.cpp `top_n_sigma`. Sigma-based truncation. -1 = off."
|
||||
/>
|
||||
) : null}
|
||||
{advCaps.repeatLastN ? (
|
||||
<ParamSlider
|
||||
label="Repeat Last N"
|
||||
value={params.repeatLastN ?? 0}
|
||||
min={-1}
|
||||
max={2048}
|
||||
step={1}
|
||||
onChange={(v) =>
|
||||
set("repeatLastN")(v === 0 ? null : v)
|
||||
}
|
||||
displayValue={
|
||||
params.repeatLastN == null
|
||||
? "Off"
|
||||
: params.repeatLastN === -1
|
||||
? "Ctx"
|
||||
: undefined
|
||||
}
|
||||
info="llama.cpp `repeat_last_n`. Token window the repetition penalty considers. 0 = off, -1 = full context."
|
||||
/>
|
||||
) : null}
|
||||
{advCaps.dynatempRange ? (
|
||||
<ParamSlider
|
||||
label="Dynatemp Range"
|
||||
value={params.dynatempRange ?? 0}
|
||||
min={0}
|
||||
max={5}
|
||||
step={0.1}
|
||||
onChange={(v) =>
|
||||
set("dynatempRange")(v === 0 ? null : v)
|
||||
}
|
||||
displayValue={
|
||||
params.dynatempRange == null || params.dynatempRange === 0
|
||||
? "Off"
|
||||
: undefined
|
||||
}
|
||||
info="llama.cpp `dynatemp_range`. Dynamic temperature swing around base temperature. 0 = off."
|
||||
/>
|
||||
) : null}
|
||||
{advCaps.dynatempExponent ? (
|
||||
<ParamSlider
|
||||
label="Dynatemp Exponent"
|
||||
value={params.dynatempExponent ?? 1}
|
||||
min={0}
|
||||
max={5}
|
||||
step={0.1}
|
||||
onChange={(v) => set("dynatempExponent")(v)}
|
||||
info="llama.cpp `dynatemp_exponent`. Curve exponent, pairs with Dynatemp Range."
|
||||
/>
|
||||
) : null}
|
||||
{advCaps.mirostat ? (
|
||||
<ParamSlider
|
||||
label="Mirostat"
|
||||
value={params.mirostat ?? 0}
|
||||
min={0}
|
||||
max={2}
|
||||
step={1}
|
||||
onChange={(v) =>
|
||||
set("mirostat")(v === 0 ? null : v)
|
||||
}
|
||||
displayValue={
|
||||
params.mirostat == null || params.mirostat === 0
|
||||
? "Off"
|
||||
: params.mirostat === 1
|
||||
? "v1"
|
||||
: "v2"
|
||||
}
|
||||
info="llama.cpp `mirostat`. Target-entropy sampler. 0 = off, 1 = Mirostat v1, 2 = Mirostat v2."
|
||||
/>
|
||||
) : null}
|
||||
{advCaps.mirostatTau ? (
|
||||
<ParamSlider
|
||||
label="Mirostat Tau"
|
||||
value={params.mirostatTau ?? 5}
|
||||
min={0}
|
||||
max={10}
|
||||
step={0.1}
|
||||
onChange={(v) => set("mirostatTau")(v)}
|
||||
info="llama.cpp `mirostat_tau`. Target entropy. Higher = more diverse."
|
||||
/>
|
||||
) : null}
|
||||
{advCaps.mirostatEta ? (
|
||||
<ParamSlider
|
||||
label="Mirostat Eta"
|
||||
value={params.mirostatEta ?? 0.1}
|
||||
min={0}
|
||||
max={1}
|
||||
step={0.01}
|
||||
onChange={(v) => set("mirostatEta")(v)}
|
||||
info="llama.cpp `mirostat_eta`. Learning rate for the entropy controller."
|
||||
/>
|
||||
) : null}
|
||||
{advCaps.topA ? (
|
||||
<ParamSlider
|
||||
label="Top A"
|
||||
value={params.topA ?? 0}
|
||||
min={0}
|
||||
max={1}
|
||||
step={0.05}
|
||||
onChange={(v) =>
|
||||
set("topA")(v === 0 ? null : v)
|
||||
}
|
||||
displayValue={
|
||||
params.topA == null || params.topA === 0 ? "Off" : undefined
|
||||
}
|
||||
info="OpenRouter `top_a`. Tail-cut sampler scaled by the top token's probability. 0 = off."
|
||||
/>
|
||||
) : null}
|
||||
{advCaps.dryMultiplier ? (
|
||||
<ParamSlider
|
||||
label="DRY Multiplier"
|
||||
value={params.dryMultiplier ?? 0}
|
||||
min={0}
|
||||
max={3}
|
||||
step={0.1}
|
||||
onChange={(v) =>
|
||||
set("dryMultiplier")(v === 0 ? null : v)
|
||||
}
|
||||
displayValue={
|
||||
params.dryMultiplier == null || params.dryMultiplier === 0
|
||||
? "Off"
|
||||
: undefined
|
||||
}
|
||||
info="llama.cpp DRY master switch (unlocks base / allowed length / penalty last N). 0 = off."
|
||||
/>
|
||||
) : null}
|
||||
{advCaps.dryBase && (params.dryMultiplier ?? 0) > 0 ? (
|
||||
<ParamSlider
|
||||
label="DRY Base"
|
||||
value={params.dryBase ?? 1.75}
|
||||
min={0}
|
||||
max={5}
|
||||
step={0.05}
|
||||
onChange={(v) => set("dryBase")(v)}
|
||||
info="llama.cpp `dry_base`. Exponential base for the DRY penalty. Default 1.75."
|
||||
/>
|
||||
) : null}
|
||||
{advCaps.dryAllowedLength && (params.dryMultiplier ?? 0) > 0 ? (
|
||||
<ParamSlider
|
||||
label="DRY Allowed Length"
|
||||
value={params.dryAllowedLength ?? 2}
|
||||
min={0}
|
||||
max={20}
|
||||
step={1}
|
||||
onChange={(v) => set("dryAllowedLength")(v)}
|
||||
info="llama.cpp `dry_allowed_length`. Repeats up to this length are not penalised. Default 2."
|
||||
/>
|
||||
) : null}
|
||||
{advCaps.dryPenaltyLastN && (params.dryMultiplier ?? 0) > 0 ? (
|
||||
<ParamSlider
|
||||
label="DRY Penalty Last N"
|
||||
value={params.dryPenaltyLastN ?? 0}
|
||||
min={-1}
|
||||
max={2048}
|
||||
step={1}
|
||||
onChange={(v) =>
|
||||
set("dryPenaltyLastN")(v === 0 ? null : v)
|
||||
}
|
||||
displayValue={
|
||||
params.dryPenaltyLastN == null
|
||||
? "Off"
|
||||
: params.dryPenaltyLastN === -1
|
||||
? "Ctx"
|
||||
: undefined
|
||||
}
|
||||
info="llama.cpp `dry_penalty_last_n`. Token window the DRY penalty considers. 0 = off, -1 = full context."
|
||||
/>
|
||||
) : null}
|
||||
{advCaps.xtcProbability ? (
|
||||
<ParamSlider
|
||||
label="XTC Probability"
|
||||
value={params.xtcProbability ?? 0}
|
||||
min={0}
|
||||
max={1}
|
||||
step={0.01}
|
||||
onChange={(v) =>
|
||||
set("xtcProbability")(v === 0 ? null : v)
|
||||
}
|
||||
displayValue={
|
||||
params.xtcProbability == null || params.xtcProbability === 0
|
||||
? "Off"
|
||||
: undefined
|
||||
}
|
||||
info="llama.cpp XTC (eXclude Top Choices). Master switch. 0 = off."
|
||||
/>
|
||||
) : null}
|
||||
{advCaps.xtcThreshold && (params.xtcProbability ?? 0) > 0 ? (
|
||||
<ParamSlider
|
||||
label="XTC Threshold"
|
||||
value={params.xtcThreshold ?? 0.1}
|
||||
min={0}
|
||||
max={1}
|
||||
step={0.01}
|
||||
onChange={(v) => set("xtcThreshold")(v)}
|
||||
info="llama.cpp `xtc_threshold`. Minimum probability for a token to be removable by XTC. Default 0.1."
|
||||
/>
|
||||
) : null}
|
||||
{advCaps.minKeep ? (
|
||||
<ParamSlider
|
||||
label="Min Keep"
|
||||
value={params.minKeep ?? 0}
|
||||
min={0}
|
||||
max={10}
|
||||
step={1}
|
||||
onChange={(v) =>
|
||||
set("minKeep")(v === 0 ? null : v)
|
||||
}
|
||||
displayValue={
|
||||
params.minKeep == null || params.minKeep === 0
|
||||
? "Off"
|
||||
: undefined
|
||||
}
|
||||
info="llama.cpp `min_keep`. Minimum tokens retained past all sampler filters."
|
||||
/>
|
||||
) : null}
|
||||
{advCaps.minTokens ? (
|
||||
<ParamSlider
|
||||
label="Min Tokens"
|
||||
value={params.minTokens ?? 0}
|
||||
min={0}
|
||||
max={512}
|
||||
step={1}
|
||||
onChange={(v) =>
|
||||
set("minTokens")(v === 0 ? null : v)
|
||||
}
|
||||
displayValue={
|
||||
params.minTokens == null || params.minTokens === 0
|
||||
? "Off"
|
||||
: undefined
|
||||
}
|
||||
info="llama.cpp + vLLM. Minimum tokens before stop / EOS can fire."
|
||||
/>
|
||||
) : null}
|
||||
{advCaps.truncatePromptTokens ? (
|
||||
<ParamSlider
|
||||
label="Truncate Prompt"
|
||||
value={params.truncatePromptTokens ?? 0}
|
||||
min={0}
|
||||
max={32768}
|
||||
step={64}
|
||||
onChange={(v) =>
|
||||
set("truncatePromptTokens")(v === 0 ? null : v)
|
||||
}
|
||||
displayValue={
|
||||
params.truncatePromptTokens == null ||
|
||||
params.truncatePromptTokens === 0
|
||||
? "Off"
|
||||
: undefined
|
||||
}
|
||||
info="vLLM `truncate_prompt_tokens`. Left-truncate the prompt to this many tokens. 0 = off."
|
||||
/>
|
||||
) : null}
|
||||
{advCaps.nKeep ? (
|
||||
<ParamSlider
|
||||
label="N Keep"
|
||||
value={params.nKeep ?? 0}
|
||||
min={-1}
|
||||
max={1024}
|
||||
step={1}
|
||||
onChange={(v) =>
|
||||
set("nKeep")(v === 0 ? null : v)
|
||||
}
|
||||
displayValue={
|
||||
params.nKeep == null
|
||||
? "Off"
|
||||
: params.nKeep === -1
|
||||
? "All"
|
||||
: undefined
|
||||
}
|
||||
info="llama.cpp `n_keep`. Tokens to retain when the context is shifted. 0 = off, -1 = keep all."
|
||||
/>
|
||||
) : null}
|
||||
{advCaps.nProbs ? (
|
||||
<ParamSlider
|
||||
label="N Probs"
|
||||
value={params.nProbs ?? 0}
|
||||
min={0}
|
||||
max={20}
|
||||
step={1}
|
||||
onChange={(v) =>
|
||||
set("nProbs")(v === 0 ? null : v)
|
||||
}
|
||||
displayValue={
|
||||
params.nProbs == null || params.nProbs === 0
|
||||
? "Off"
|
||||
: undefined
|
||||
}
|
||||
info="llama.cpp `n_probs`. Return the top-N token probabilities per token (diagnostic)."
|
||||
/>
|
||||
) : null}
|
||||
{advCaps.ignoreEos ? (
|
||||
<div className="flex items-center justify-between gap-3">
|
||||
<div className="flex min-w-0 items-center gap-1.5">
|
||||
<span className="min-w-0 text-[13px] font-medium leading-[1.25] tracking-nav text-nav-fg">
|
||||
Ignore EOS
|
||||
</span>
|
||||
<InfoHint>
|
||||
llama.cpp + vLLM. Keep generating past the model's
|
||||
end-of-sequence token. Useful for forcing long replies.
|
||||
</InfoHint>
|
||||
</div>
|
||||
<Switch
|
||||
className="panel-switch shrink-0"
|
||||
checked={Boolean(params.ignoreEos)}
|
||||
onCheckedChange={(v) => set("ignoreEos")(v ? true : null)}
|
||||
aria-label="Ignore EOS"
|
||||
/>
|
||||
</div>
|
||||
) : null}
|
||||
{advCaps.skipSpecialTokens ? (
|
||||
<div className="flex items-center justify-between gap-3">
|
||||
<div className="flex min-w-0 items-center gap-1.5">
|
||||
<span className="min-w-0 text-[13px] font-medium leading-[1.25] tracking-nav text-nav-fg">
|
||||
Skip Special Tokens
|
||||
</span>
|
||||
<InfoHint>
|
||||
vLLM `skip_special_tokens` (default on). Off keeps
|
||||
chat-template markers like `<|im_end|>` in output.
|
||||
</InfoHint>
|
||||
</div>
|
||||
<Switch
|
||||
className="panel-switch shrink-0"
|
||||
checked={params.skipSpecialTokens ?? true}
|
||||
onCheckedChange={(v) =>
|
||||
set("skipSpecialTokens")(v ? null : false)
|
||||
}
|
||||
aria-label="Skip special tokens"
|
||||
/>
|
||||
</div>
|
||||
) : null}
|
||||
{advCaps.spacesBetweenSpecialTokens ? (
|
||||
<div className="flex items-center justify-between gap-3">
|
||||
<div className="flex min-w-0 items-center gap-1.5">
|
||||
<span className="min-w-0 text-[13px] font-medium leading-[1.25] tracking-nav text-nav-fg">
|
||||
Spaces Between Special Tokens
|
||||
</span>
|
||||
<InfoHint>
|
||||
vLLM `spaces_between_special_tokens`. Default on.
|
||||
</InfoHint>
|
||||
</div>
|
||||
<Switch
|
||||
className="panel-switch shrink-0"
|
||||
checked={params.spacesBetweenSpecialTokens ?? true}
|
||||
onCheckedChange={(v) =>
|
||||
set("spacesBetweenSpecialTokens")(v ? null : false)
|
||||
}
|
||||
aria-label="Spaces between special tokens"
|
||||
/>
|
||||
</div>
|
||||
) : null}
|
||||
{advCaps.includeStopStrInOutput ? (
|
||||
<div className="flex items-center justify-between gap-3">
|
||||
<div className="flex min-w-0 items-center gap-1.5">
|
||||
<span className="min-w-0 text-[13px] font-medium leading-[1.25] tracking-nav text-nav-fg">
|
||||
Include Stop String
|
||||
</span>
|
||||
<InfoHint>
|
||||
vLLM `include_stop_str_in_output`. Echo the matched stop
|
||||
string back in the response (useful for agentic tools).
|
||||
</InfoHint>
|
||||
</div>
|
||||
<Switch
|
||||
className="panel-switch shrink-0"
|
||||
checked={Boolean(params.includeStopStrInOutput)}
|
||||
onCheckedChange={(v) =>
|
||||
set("includeStopStrInOutput")(v ? true : null)
|
||||
}
|
||||
aria-label="Include stop string in output"
|
||||
/>
|
||||
</div>
|
||||
) : null}
|
||||
{advCaps.cachePrompt ? (
|
||||
<div className="flex items-center justify-between gap-3">
|
||||
<div className="flex min-w-0 items-center gap-1.5">
|
||||
<span className="min-w-0 text-[13px] font-medium leading-[1.25] tracking-nav text-nav-fg">
|
||||
Cache Prompt
|
||||
</span>
|
||||
<InfoHint>
|
||||
llama.cpp `cache_prompt`. Default on. Reuses the KV cache
|
||||
across requests with shared prefixes.
|
||||
</InfoHint>
|
||||
</div>
|
||||
<Switch
|
||||
className="panel-switch shrink-0"
|
||||
checked={params.cachePrompt ?? true}
|
||||
onCheckedChange={(v) =>
|
||||
set("cachePrompt")(v ? null : false)
|
||||
}
|
||||
aria-label="Cache prompt"
|
||||
/>
|
||||
</div>
|
||||
) : null}
|
||||
{advCaps.returnTokens ? (
|
||||
<div className="flex items-center justify-between gap-3">
|
||||
<div className="flex min-w-0 items-center gap-1.5">
|
||||
<span className="min-w-0 text-[13px] font-medium leading-[1.25] tracking-nav text-nav-fg">
|
||||
Return Tokens
|
||||
</span>
|
||||
<InfoHint>
|
||||
llama.cpp `return_tokens`. Include the raw token ids in
|
||||
the response (debug).
|
||||
</InfoHint>
|
||||
</div>
|
||||
<Switch
|
||||
className="panel-switch shrink-0"
|
||||
checked={Boolean(params.returnTokens)}
|
||||
onCheckedChange={(v) =>
|
||||
set("returnTokens")(v ? true : null)
|
||||
}
|
||||
aria-label="Return tokens"
|
||||
/>
|
||||
</div>
|
||||
) : null}
|
||||
{advCaps.timingsPerToken ? (
|
||||
<div className="flex items-center justify-between gap-3">
|
||||
<div className="flex min-w-0 items-center gap-1.5">
|
||||
<span className="min-w-0 text-[13px] font-medium leading-[1.25] tracking-nav text-nav-fg">
|
||||
Timings Per Token
|
||||
</span>
|
||||
<InfoHint>
|
||||
llama.cpp `timings_per_token`. Per-token wall-clock
|
||||
timings in the response (perf debug).
|
||||
</InfoHint>
|
||||
</div>
|
||||
<Switch
|
||||
className="panel-switch shrink-0"
|
||||
checked={Boolean(params.timingsPerToken)}
|
||||
onCheckedChange={(v) =>
|
||||
set("timingsPerToken")(v ? true : null)
|
||||
}
|
||||
aria-label="Timings per token"
|
||||
/>
|
||||
</div>
|
||||
) : null}
|
||||
{advCaps.postSamplingProbs ? (
|
||||
<div className="flex items-center justify-between gap-3">
|
||||
<div className="flex min-w-0 items-center gap-1.5">
|
||||
<span className="min-w-0 text-[13px] font-medium leading-[1.25] tracking-nav text-nav-fg">
|
||||
Post-Sampling Probs
|
||||
</span>
|
||||
<InfoHint>
|
||||
llama.cpp `post_sampling_probs`. Report the
|
||||
post-sampling distribution (sampler debug).
|
||||
</InfoHint>
|
||||
</div>
|
||||
<Switch
|
||||
className="panel-switch shrink-0"
|
||||
checked={Boolean(params.postSamplingProbs)}
|
||||
onCheckedChange={(v) =>
|
||||
set("postSamplingProbs")(v ? true : null)
|
||||
}
|
||||
aria-label="Post-sampling probs"
|
||||
/>
|
||||
</div>
|
||||
) : null}
|
||||
</div>
|
||||
</CollapsibleSection>
|
||||
) : null}
|
||||
|
||||
{!isExternalModel ? (
|
||||
<CollapsibleSection label="Tools">
|
||||
<div className="flex flex-col gap-5 pt-1">
|
||||
|
|
|
|||
|
|
@ -13,6 +13,11 @@ export interface Preset {
|
|||
params: InferenceParams;
|
||||
}
|
||||
|
||||
// Fields that belong to a preset. Sampling knobs are included so a
|
||||
// user can save a preset that fixes their preferred decoding style.
|
||||
// Operational knobs (serviceTier, parallelToolCalls) and per-request
|
||||
// determinism state (seed) are intentionally excluded so switching
|
||||
// presets does not silently change request routing.
|
||||
export type PresetOwnedParams = Pick<
|
||||
InferenceParams,
|
||||
| "temperature"
|
||||
|
|
@ -21,6 +26,8 @@ export type PresetOwnedParams = Pick<
|
|||
| "minP"
|
||||
| "repetitionPenalty"
|
||||
| "presencePenalty"
|
||||
| "frequencyPenalty"
|
||||
| "stop"
|
||||
| "maxTokens"
|
||||
| "systemPrompt"
|
||||
>;
|
||||
|
|
@ -103,11 +110,22 @@ export function getPresetOwnedParams(
|
|||
minP: params.minP,
|
||||
repetitionPenalty: params.repetitionPenalty,
|
||||
presencePenalty: params.presencePenalty,
|
||||
frequencyPenalty: params.frequencyPenalty,
|
||||
stop: params.stop,
|
||||
maxTokens: params.maxTokens,
|
||||
systemPrompt: params.systemPrompt,
|
||||
};
|
||||
}
|
||||
|
||||
function stopArraysEqual(a: string[], b: string[]): boolean {
|
||||
if (a === b) return true;
|
||||
if (a.length !== b.length) return false;
|
||||
for (let i = 0; i < a.length; i += 1) {
|
||||
if (a[i] !== b[i]) return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
export function isSamePresetConfig(
|
||||
a: InferenceParams,
|
||||
b: InferenceParams,
|
||||
|
|
@ -121,6 +139,8 @@ export function isSamePresetConfig(
|
|||
left.minP === right.minP &&
|
||||
left.repetitionPenalty === right.repetitionPenalty &&
|
||||
left.presencePenalty === right.presencePenalty &&
|
||||
left.frequencyPenalty === right.frequencyPenalty &&
|
||||
stopArraysEqual(left.stop, right.stop) &&
|
||||
left.maxTokens === right.maxTokens &&
|
||||
left.systemPrompt === right.systemPrompt
|
||||
);
|
||||
|
|
|
|||
File diff suppressed because it is too large
Load diff
|
|
@ -406,11 +406,45 @@ const PERSISTED_INFERENCE_PARAM_KEYS = [
|
|||
"minP",
|
||||
"repetitionPenalty",
|
||||
"presencePenalty",
|
||||
"frequencyPenalty",
|
||||
"seed",
|
||||
"stop",
|
||||
"serviceTier",
|
||||
"parallelToolCalls",
|
||||
"maxSeqLength",
|
||||
"maxTokens",
|
||||
"systemPrompt",
|
||||
"trustRemoteCode",
|
||||
"fastMode",
|
||||
// Extended llama.cpp / vLLM / OpenRouter samplers exposed by PR #5711.
|
||||
"typicalP",
|
||||
"topNSigma",
|
||||
"repeatLastN",
|
||||
"dynatempRange",
|
||||
"dynatempExponent",
|
||||
"mirostat",
|
||||
"mirostatTau",
|
||||
"mirostatEta",
|
||||
"topA",
|
||||
"dryMultiplier",
|
||||
"dryBase",
|
||||
"dryAllowedLength",
|
||||
"dryPenaltyLastN",
|
||||
"xtcProbability",
|
||||
"xtcThreshold",
|
||||
"minKeep",
|
||||
"ignoreEos",
|
||||
"minTokens",
|
||||
"skipSpecialTokens",
|
||||
"spacesBetweenSpecialTokens",
|
||||
"includeStopStrInOutput",
|
||||
"truncatePromptTokens",
|
||||
"nKeep",
|
||||
"nProbs",
|
||||
"cachePrompt",
|
||||
"returnTokens",
|
||||
"timingsPerToken",
|
||||
"postSamplingProbs",
|
||||
] as const satisfies readonly PersistedInferenceParamKey[];
|
||||
|
||||
const SCALAR_SETTING_KEYS = [
|
||||
|
|
|
|||
|
|
@ -314,12 +314,81 @@ export interface OpenAIChatCompletionsRequest {
|
|||
* the Anthropic provider with `code_execution` in `enabled_tools`.
|
||||
*/
|
||||
anthropic_code_exec_container_id?: string | null;
|
||||
/**
|
||||
* Anthropic fast-mode toggle. Opus 4.6 / 4.7 only; backend drops
|
||||
* silently on every other model + provider. See
|
||||
* https://platform.claude.com/docs/en/build-with-claude/fast-mode
|
||||
*/
|
||||
/** OpenAI Chat only. Range -2..2. */
|
||||
frequency_penalty?: number;
|
||||
/** OAI Chat + most OAI-compat. Responses + Anthropic drop. */
|
||||
seed?: number;
|
||||
/** OAI Chat caps at 4; Anthropic mapped to `stop_sequences`. */
|
||||
stop?: string[];
|
||||
/** Per-provider enum (see getServiceTierOptions); external_provider.py drops unsupported values. */
|
||||
service_tier?:
|
||||
| "auto"
|
||||
| "default"
|
||||
| "flex"
|
||||
| "priority"
|
||||
| "scale"
|
||||
| "standard_only";
|
||||
/** Anthropic inverts to `disable_parallel_tool_use`. */
|
||||
parallel_tool_calls?: boolean;
|
||||
/** llama.cpp `typ_p`. 1.0 disables. */
|
||||
typical_p?: number;
|
||||
/** llama.cpp `top_n_sigma`. -1 disables. */
|
||||
top_n_sigma?: number;
|
||||
/** llama.cpp `repeat_last_n`. 0 disables, -1 = ctx-size. */
|
||||
repeat_last_n?: number;
|
||||
/** llama.cpp `dynatemp_range`. 0 disables. */
|
||||
dynatemp_range?: number;
|
||||
/** llama.cpp `dynatemp_exponent`. Pairs with dynatemp_range. */
|
||||
dynatemp_exponent?: number;
|
||||
/** llama.cpp `mirostat` (0/1/2). 0 disables. */
|
||||
mirostat?: number;
|
||||
mirostat_tau?: number;
|
||||
mirostat_eta?: number;
|
||||
/** OpenRouter `top_a`. https://openrouter.ai/docs/api/reference/parameters */
|
||||
top_a?: number;
|
||||
/** Anthropic Opus 4.6 / 4.7 only. https://platform.claude.com/docs/en/build-with-claude/fast-mode */
|
||||
fast_mode?: boolean | null;
|
||||
/**
|
||||
* llama.cpp DRY sampler (4 fields). `dry_multiplier=0` disables.
|
||||
* https://github.com/ggml-org/llama.cpp/blob/master/tools/server/README.md
|
||||
*/
|
||||
dry_multiplier?: number;
|
||||
/** Default 1.75. */
|
||||
dry_base?: number;
|
||||
/** Default 2. */
|
||||
dry_allowed_length?: number;
|
||||
/** 0 disables, -1 = ctx-size. */
|
||||
dry_penalty_last_n?: number;
|
||||
/** llama.cpp XTC. 0 disables. */
|
||||
xtc_probability?: number;
|
||||
/** Default 0.1. */
|
||||
xtc_threshold?: number;
|
||||
/** llama.cpp `min_keep`. */
|
||||
min_keep?: number;
|
||||
/** Continue past EOS. llama.cpp + vLLM. */
|
||||
ignore_eos?: boolean;
|
||||
/** Min tokens before stop / EOS. llama.cpp + vLLM. */
|
||||
min_tokens?: number;
|
||||
/** vLLM only. */
|
||||
skip_special_tokens?: boolean;
|
||||
/** vLLM only. */
|
||||
spaces_between_special_tokens?: boolean;
|
||||
/** vLLM only. Useful for agentic tools. */
|
||||
include_stop_str_in_output?: boolean;
|
||||
/** vLLM only. Left-truncate the prompt. */
|
||||
truncate_prompt_tokens?: number;
|
||||
/** llama.cpp `n_keep`. -1 = keep all. */
|
||||
n_keep?: number;
|
||||
/** llama.cpp `n_probs`. */
|
||||
n_probs?: number;
|
||||
/** llama.cpp `cache_prompt`. */
|
||||
cache_prompt?: boolean;
|
||||
/** llama.cpp `return_tokens` (debug). */
|
||||
return_tokens?: boolean;
|
||||
/** llama.cpp `timings_per_token` (perf debug). */
|
||||
timings_per_token?: boolean;
|
||||
/** llama.cpp `post_sampling_probs` (sampler debug). */
|
||||
post_sampling_probs?: boolean;
|
||||
}
|
||||
|
||||
export interface OpenAIChatDelta {
|
||||
|
|
|
|||
|
|
@ -1,6 +1,16 @@
|
|||
// SPDX-License-Identifier: AGPL-3.0-only
|
||||
// Copyright 2026-present the Unsloth AI Inc. team. All rights reserved. See /studio/LICENSE.AGPL-3.0
|
||||
|
||||
export type ServiceTier =
|
||||
| "auto"
|
||||
| "default"
|
||||
| "flex"
|
||||
| "priority"
|
||||
| "scale"
|
||||
| "standard_only";
|
||||
|
||||
// null = field omitted from wire (provider default).
|
||||
// Per-provider gates in provider-capabilities.ts.
|
||||
export interface InferenceParams {
|
||||
temperature: number;
|
||||
topP: number;
|
||||
|
|
@ -8,17 +18,77 @@ export interface InferenceParams {
|
|||
minP: number;
|
||||
repetitionPenalty: number;
|
||||
presencePenalty: number;
|
||||
/** OpenAI Chat only; rejected by Responses + Anthropic. */
|
||||
frequencyPenalty: number;
|
||||
/** Determinism seed. OpenAI Chat + most OAI-compat backends only. */
|
||||
seed: number | null;
|
||||
/** OAI Chat `stop` / Anthropic `stop_sequences`. OAI caps at 4. */
|
||||
stop: string[];
|
||||
/** Per-provider enum via `getServiceTierOptions`. `null` = provider default. */
|
||||
serviceTier: ServiceTier | null;
|
||||
/** Anthropic inverts to `disable_parallel_tool_use`. */
|
||||
parallelToolCalls: boolean;
|
||||
/** llama.cpp `typ_p`. 1.0 disables. */
|
||||
typicalP: number | null;
|
||||
/** llama.cpp `top_n_sigma`. -1 disables. */
|
||||
topNSigma: number | null;
|
||||
/** llama.cpp `repeat_last_n`. 0 disables, -1 = ctx-size. */
|
||||
repeatLastN: number | null;
|
||||
/** llama.cpp `dynatemp_range`. 0 disables. */
|
||||
dynatempRange: number | null;
|
||||
/** llama.cpp `dynatemp_exponent`. Pairs with dynatempRange. */
|
||||
dynatempExponent: number | null;
|
||||
/** llama.cpp `mirostat` (0/1/2). 0 disables. */
|
||||
mirostat: number | null;
|
||||
mirostatTau: number | null;
|
||||
mirostatEta: number | null;
|
||||
/** OpenRouter `top_a`. Range [0, 1]. */
|
||||
topA: number | null;
|
||||
/** 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;
|
||||
/** Default 2. */
|
||||
dryAllowedLength: number | null;
|
||||
/** 0 disables, -1 = ctx-size. */
|
||||
dryPenaltyLastN: number | null;
|
||||
/** llama.cpp XTC: probability is the master switch (0 disables). */
|
||||
xtcProbability: number | null;
|
||||
/** Default 0.1. */
|
||||
xtcThreshold: number | null;
|
||||
/** llama.cpp `min_keep`: min tokens past all filters. 0 disables. */
|
||||
minKeep: number | null;
|
||||
/** Continue past EOS. llama.cpp + vLLM. */
|
||||
ignoreEos: boolean | null;
|
||||
/** Min tokens before stop / EOS can fire. llama.cpp + vLLM. */
|
||||
minTokens: number | null;
|
||||
/** vLLM only. Default true; forward only when false. */
|
||||
skipSpecialTokens: boolean | null;
|
||||
/** vLLM only. Default true; forward only when false. */
|
||||
spacesBetweenSpecialTokens: boolean | null;
|
||||
/** vLLM only. Useful for agentic tools needing the matched stop string echoed. */
|
||||
includeStopStrInOutput: boolean | null;
|
||||
/** vLLM only. Left-truncate the prompt. */
|
||||
truncatePromptTokens: number | null;
|
||||
/** llama.cpp `n_keep`. 0 disables, -1 = keep all. */
|
||||
nKeep: number | null;
|
||||
/** llama.cpp `n_probs`: top-N token probabilities per token. */
|
||||
nProbs: number | null;
|
||||
/** llama.cpp `cache_prompt`. Default true; forward only when false. */
|
||||
cachePrompt: boolean | null;
|
||||
/** llama.cpp `return_tokens` (debug). */
|
||||
returnTokens: boolean | null;
|
||||
/** llama.cpp `timings_per_token` (perf debug). */
|
||||
timingsPerToken: boolean | null;
|
||||
/** llama.cpp `post_sampling_probs` (sampler debug). */
|
||||
postSamplingProbs: boolean | null;
|
||||
maxSeqLength: number;
|
||||
maxTokens: number;
|
||||
systemPrompt: string;
|
||||
checkpoint: string;
|
||||
/** Allow loading models with custom code (e.g. NVIDIA Nemotron). Only enable for repos you trust. */
|
||||
/** Trust custom model code (e.g. NVIDIA Nemotron). Only for trusted repos. */
|
||||
trustRemoteCode?: boolean;
|
||||
/**
|
||||
* Anthropic fast-mode toggle. Opus 4.6 / 4.7 only; higher OTPS at
|
||||
* 6x standard Opus pricing. Default false.
|
||||
* https://platform.claude.com/docs/en/build-with-claude/fast-mode
|
||||
*/
|
||||
/** Anthropic Opus 4.6 / 4.7 only. 6x pricing for higher OTPS. */
|
||||
fastMode?: boolean;
|
||||
}
|
||||
|
||||
|
|
@ -29,6 +99,39 @@ export const DEFAULT_INFERENCE_PARAMS: InferenceParams = {
|
|||
minP: 0.01,
|
||||
repetitionPenalty: 1.0,
|
||||
presencePenalty: 0.0,
|
||||
frequencyPenalty: 0.0,
|
||||
seed: null,
|
||||
stop: [],
|
||||
serviceTier: null,
|
||||
parallelToolCalls: true,
|
||||
typicalP: null,
|
||||
topNSigma: null,
|
||||
repeatLastN: null,
|
||||
dynatempRange: null,
|
||||
dynatempExponent: null,
|
||||
mirostat: null,
|
||||
mirostatTau: null,
|
||||
mirostatEta: null,
|
||||
topA: null,
|
||||
dryMultiplier: null,
|
||||
dryBase: null,
|
||||
dryAllowedLength: null,
|
||||
dryPenaltyLastN: null,
|
||||
xtcProbability: null,
|
||||
xtcThreshold: null,
|
||||
minKeep: null,
|
||||
ignoreEos: null,
|
||||
minTokens: null,
|
||||
skipSpecialTokens: null,
|
||||
spacesBetweenSpecialTokens: null,
|
||||
includeStopStrInOutput: null,
|
||||
truncatePromptTokens: null,
|
||||
nKeep: null,
|
||||
nProbs: null,
|
||||
cachePrompt: null,
|
||||
returnTokens: null,
|
||||
timingsPerToken: null,
|
||||
postSamplingProbs: null,
|
||||
maxSeqLength: 4096,
|
||||
maxTokens: 8192,
|
||||
systemPrompt: "",
|
||||
|
|
|
|||
|
|
@ -40,10 +40,23 @@ const NUMERIC_INFERENCE_FIELDS = [
|
|||
"minP",
|
||||
"repetitionPenalty",
|
||||
"presencePenalty",
|
||||
"frequencyPenalty",
|
||||
"maxSeqLength",
|
||||
"maxTokens",
|
||||
] as const satisfies readonly (keyof PersistedInferenceParams)[];
|
||||
|
||||
// `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",
|
||||
"flex",
|
||||
"priority",
|
||||
"scale",
|
||||
"standard_only",
|
||||
]);
|
||||
|
||||
const CHAT_PRESET_SOURCES = new Set<string>([
|
||||
"builtin-default",
|
||||
"custom",
|
||||
|
|
@ -140,6 +153,87 @@ function sanitizeInferenceParams(
|
|||
if (typeof value.trustRemoteCode === "boolean") {
|
||||
params.trustRemoteCode = value.trustRemoteCode;
|
||||
}
|
||||
// seed: nullable integer (null = no seed on the wire).
|
||||
if (value.seed === null) {
|
||||
params.seed = null;
|
||||
} else if (typeof value.seed === "number" && Number.isInteger(value.seed)) {
|
||||
params.seed = value.seed;
|
||||
}
|
||||
// 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);
|
||||
}
|
||||
if (value.serviceTier === null) {
|
||||
params.serviceTier = null;
|
||||
} else if (
|
||||
typeof value.serviceTier === "string" &&
|
||||
VALID_SERVICE_TIERS.has(value.serviceTier)
|
||||
) {
|
||||
params.serviceTier = value.serviceTier as PersistedInferenceParams["serviceTier"];
|
||||
}
|
||||
if (typeof value.parallelToolCalls === "boolean") {
|
||||
params.parallelToolCalls = value.parallelToolCalls;
|
||||
}
|
||||
// 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 (
|
||||
typeof value.typicalP === "number" &&
|
||||
Number.isFinite(value.typicalP)
|
||||
) {
|
||||
params.typicalP = value.typicalP;
|
||||
}
|
||||
// Nullable numeric samplers (same handling as typicalP/seed).
|
||||
for (const key of [
|
||||
"topNSigma",
|
||||
"repeatLastN",
|
||||
"dynatempRange",
|
||||
"dynatempExponent",
|
||||
"mirostat",
|
||||
"mirostatTau",
|
||||
"mirostatEta",
|
||||
"topA",
|
||||
"dryMultiplier",
|
||||
"dryBase",
|
||||
"dryAllowedLength",
|
||||
"dryPenaltyLastN",
|
||||
"xtcProbability",
|
||||
"xtcThreshold",
|
||||
"minKeep",
|
||||
"minTokens",
|
||||
"truncatePromptTokens",
|
||||
"nKeep",
|
||||
"nProbs",
|
||||
] as const) {
|
||||
const raw = value[key];
|
||||
if (raw === null) {
|
||||
(params as Record<string, unknown>)[key] = null;
|
||||
} else if (typeof raw === "number" && Number.isFinite(raw)) {
|
||||
(params as Record<string, unknown>)[key] = raw;
|
||||
}
|
||||
}
|
||||
// Nullable booleans (ignoreEos, skip/spaces special-tokens, include-stop,
|
||||
// cache_prompt, return_tokens, timings_per_token, post_sampling_probs).
|
||||
for (const key of [
|
||||
"ignoreEos",
|
||||
"skipSpecialTokens",
|
||||
"spacesBetweenSpecialTokens",
|
||||
"includeStopStrInOutput",
|
||||
"cachePrompt",
|
||||
"returnTokens",
|
||||
"timingsPerToken",
|
||||
"postSamplingProbs",
|
||||
] as const) {
|
||||
const raw = value[key];
|
||||
if (raw === null) {
|
||||
(params as Record<string, unknown>)[key] = null;
|
||||
} else if (typeof raw === "boolean") {
|
||||
(params as Record<string, unknown>)[key] = raw;
|
||||
}
|
||||
}
|
||||
// Mirror trustRemoteCode handling so the toggle survives reload
|
||||
// and the /api/chat/settings round-trip.
|
||||
if (typeof value.fastMode === "boolean") {
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue