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.
|
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.block_index: int = -1
|
||||||
self._current_block_type: Optional[str] = None # "text" | "tool_use" | None
|
self._current_block_type: Optional[str] = None # "text" | "tool_use" | None
|
||||||
self._tool_call_states: dict = {} # delta index -> {block_index, id, name}
|
self._tool_call_states: dict = {} # delta index -> {block_index, id, name}
|
||||||
self._usage: dict = {}
|
self._usage: dict = {}
|
||||||
self._stop_reason: str = "end_turn"
|
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]:
|
def start(self, message_id: str, model: str) -> list[str]:
|
||||||
return [
|
return [
|
||||||
|
|
@ -470,6 +476,13 @@ class AnthropicPassthroughEmitter:
|
||||||
tool_calls = delta.get("tool_calls") or []
|
tool_calls = delta.get("tool_calls") or []
|
||||||
for tc in tool_calls:
|
for tc in tool_calls:
|
||||||
tc_idx = tc.get("index", 0)
|
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 {}
|
fn = tc.get("function") or {}
|
||||||
if tc_idx not in self._tool_call_states:
|
if tc_idx not in self._tool_call_states:
|
||||||
# New tool call — close prior block, open tool_use block
|
# New tool call — close prior block, open tool_use block
|
||||||
|
|
|
||||||
|
|
@ -29,30 +29,62 @@ import structlog
|
||||||
logger = structlog.get_logger(__name__)
|
logger = structlog.get_logger(__name__)
|
||||||
|
|
||||||
|
|
||||||
# Claude 4.7 (Opus/Sonnet/Haiku) removed temperature, top_p, and top_k —
|
def _normalize_stop_for_provider(
|
||||||
# the API returns 400 "<param> is deprecated for this model" if any of
|
stop: Optional[Union[str, list[str]]],
|
||||||
# them is set to a non-default value. The "Sampling parameters removed"
|
provider_info: dict[str, Any],
|
||||||
# section of the 4.7 release notes is the authoritative reference:
|
) -> Optional[Union[str, list[str]]]:
|
||||||
# https://platform.claude.com/docs/en/about-claude/models/whats-new-claude-4-7
|
"""Apply per-provider stop_max / stop_max_bytes caps and dedup.
|
||||||
# 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
|
Returns None when nothing survives the filter so callers can omit
|
||||||
# anchor keeps future versions (e.g. claude-opus-5) unaffected.
|
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:
|
def _is_openai_family_cloud(base_url: Optional[str]) -> bool:
|
||||||
"""True iff ``base_url`` points at OpenAI cloud or Azure OpenAI Foundry.
|
"""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
|
Host-anchored against subdomain-injection (api.openai.com.attacker.com).
|
||||||
path or subdomain like ``https://evil.com/api.openai.com/v1`` or
|
Gates Responses-API extensions (prompt_cache_retention, context_management,
|
||||||
``https://api.openai.com.attacker.com/v1`` (CodeQL py/incomplete-url-
|
container shell) that 400 on non-cloud OAI-compat servers. Azure Foundry
|
||||||
substring-sanitization). Used to scope cloud-only Responses-API
|
matches via .openai.azure.com suffix; leading dot blocks the apex.
|
||||||
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).
|
|
||||||
"""
|
"""
|
||||||
if not base_url:
|
if not base_url:
|
||||||
return False
|
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")
|
return host == "api.openai.com" or host.endswith(".openai.azure.com")
|
||||||
|
|
||||||
|
|
||||||
_ANTHROPIC_4_7_SAMPLING_REMOVED = re.compile(
|
_ANTHROPIC_4_7_SAMPLING_REMOVED = re.compile(r"^claude-opus-4-7(?:[-.]|$)")
|
||||||
r"^claude-(?:opus|sonnet|haiku)-4-7(?:[-.]|$)"
|
|
||||||
)
|
|
||||||
_OPENAI_REASONING_SUMMARY_UNSUPPORTED = re.compile(r"^o3(?:[-.]|$)")
|
_OPENAI_REASONING_SUMMARY_UNSUPPORTED = re.compile(r"^o3(?:[-.]|$)")
|
||||||
_OPENAI_REASONING_STATUSES = {"in_progress", "completed", "incomplete"}
|
_OPENAI_REASONING_STATUSES = {"in_progress", "completed", "incomplete"}
|
||||||
|
|
||||||
|
|
@ -866,9 +896,42 @@ class ExternalProviderClient:
|
||||||
anthropic_code_exec_container_id: Optional[str] = None,
|
anthropic_code_exec_container_id: Optional[str] = None,
|
||||||
prompt_cache_ttl: Optional[str] = None,
|
prompt_cache_ttl: Optional[str] = None,
|
||||||
compaction_threshold: Optional[int] = 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,
|
tools: Optional[list[dict[str, Any]]] = None,
|
||||||
tool_choice: Optional[Any] = None,
|
tool_choice: Optional[Any] = None,
|
||||||
fast_mode: Optional[bool] = 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,
|
stream: bool = True,
|
||||||
) -> AsyncGenerator[str, None]:
|
) -> AsyncGenerator[str, None]:
|
||||||
"""
|
"""
|
||||||
|
|
@ -877,13 +940,14 @@ class ExternalProviderClient:
|
||||||
For OpenAI-compatible providers, lines are forwarded verbatim.
|
For OpenAI-compatible providers, lines are forwarded verbatim.
|
||||||
For Anthropic, the native Messages API SSE is translated to OpenAI format.
|
For Anthropic, the native Messages API SSE is translated to OpenAI format.
|
||||||
|
|
||||||
``top_k`` and ``presence_penalty`` are forwarded only when the caller
|
Optional sampling extras (``top_k``, ``presence_penalty``,
|
||||||
supplies a value the provider accepts — the frontend's
|
``frequency_penalty``, ``seed``, ``stop``, ``service_tier``,
|
||||||
provider-capability map already filters these per provider, so we
|
``parallel_tool_calls``) are opt-in. Per-provider helpers silently
|
||||||
treat them as opt-in here.
|
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
|
``fast_mode``: Anthropic Opus 4.6/4.7 only; adds the beta header
|
||||||
dropped elsewhere); adds the beta header and ``speed: "fast"``.
|
and ``speed: "fast"``.
|
||||||
"""
|
"""
|
||||||
# tool_choice="none" hard-disables hosted/builtin tools across
|
# tool_choice="none" hard-disables hosted/builtin tools across
|
||||||
# every provider so enabled_tools cannot accidentally bill or leak.
|
# every provider so enabled_tools cannot accidentally bill or leak.
|
||||||
|
|
@ -911,6 +975,7 @@ class ExternalProviderClient:
|
||||||
reasoning_effort,
|
reasoning_effort,
|
||||||
tools,
|
tools,
|
||||||
tool_choice,
|
tool_choice,
|
||||||
|
stop = stop,
|
||||||
):
|
):
|
||||||
yield line
|
yield line
|
||||||
return
|
return
|
||||||
|
|
@ -929,6 +994,9 @@ class ExternalProviderClient:
|
||||||
prompt_cache_ttl,
|
prompt_cache_ttl,
|
||||||
compaction_threshold,
|
compaction_threshold,
|
||||||
tool_choice,
|
tool_choice,
|
||||||
|
stop = stop,
|
||||||
|
service_tier = service_tier,
|
||||||
|
parallel_tool_calls = parallel_tool_calls,
|
||||||
fast_mode = fast_mode,
|
fast_mode = fast_mode,
|
||||||
):
|
):
|
||||||
yield line
|
yield line
|
||||||
|
|
@ -954,6 +1022,8 @@ class ExternalProviderClient:
|
||||||
compaction_threshold,
|
compaction_threshold,
|
||||||
tools,
|
tools,
|
||||||
tool_choice,
|
tool_choice,
|
||||||
|
service_tier = service_tier,
|
||||||
|
parallel_tool_calls = parallel_tool_calls,
|
||||||
):
|
):
|
||||||
yield line
|
yield line
|
||||||
return
|
return
|
||||||
|
|
@ -978,6 +1048,11 @@ class ExternalProviderClient:
|
||||||
messages,
|
messages,
|
||||||
model,
|
model,
|
||||||
max_tokens,
|
max_tokens,
|
||||||
|
frequency_penalty = frequency_penalty,
|
||||||
|
seed = seed,
|
||||||
|
stop = stop,
|
||||||
|
parallel_tool_calls = parallel_tool_calls,
|
||||||
|
presence_penalty = presence_penalty,
|
||||||
):
|
):
|
||||||
yield line
|
yield line
|
||||||
return
|
return
|
||||||
|
|
@ -997,14 +1072,99 @@ class ExternalProviderClient:
|
||||||
else:
|
else:
|
||||||
body["max_tokens"] = max_tokens
|
body["max_tokens"] = max_tokens
|
||||||
|
|
||||||
# Drop fields the registry flags as unusable so reasoning-class
|
# Optional sampling extras; `seed_field` renames seed (Mistral),
|
||||||
# models with fixed defaults (Kimi k2.6 etc) don't 400 on pydantic
|
# `body_omit` strips upstream-rejected fields.
|
||||||
# default values that the route layer still fills in.
|
|
||||||
from core.inference.providers import get_provider_info
|
from core.inference.providers import get_provider_info
|
||||||
|
|
||||||
provider_info = get_provider_info(self.provider_type) or {}
|
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", ()):
|
for field in provider_info.get("body_omit", ()):
|
||||||
body.pop(field, None)
|
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
|
# Kimi thinking is a top-level body field. kimi-k2-thinking is
|
||||||
# always on (ignore the toggle); kimi-k2.6 defaults on, can be
|
# always on (ignore the toggle); kimi-k2.6 defaults on, can be
|
||||||
|
|
@ -1345,6 +1505,12 @@ class ExternalProviderClient:
|
||||||
messages: list[dict[str, Any]],
|
messages: list[dict[str, Any]],
|
||||||
model: str,
|
model: str,
|
||||||
max_tokens: Optional[int],
|
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]:
|
) -> AsyncGenerator[str, None]:
|
||||||
"""
|
"""
|
||||||
Kimi $web_search round-trip.
|
Kimi $web_search round-trip.
|
||||||
|
|
@ -1384,11 +1550,25 @@ class ExternalProviderClient:
|
||||||
if max_tokens is not None:
|
if max_tokens is not None:
|
||||||
body["max_tokens"] = max_tokens
|
body["max_tokens"] = max_tokens
|
||||||
|
|
||||||
# Strip body fields the Kimi registry declares unusable
|
# Kimi-with-search returns early before the default OAI-compat
|
||||||
# (temperature/top_p — see body_omit in providers.py).
|
# body build; re-apply provider-aware sampling/stop here.
|
||||||
from core.inference.providers import get_provider_info
|
from core.inference.providers import get_provider_info
|
||||||
|
|
||||||
provider_info = get_provider_info(self.provider_type) or {}
|
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", ()):
|
for field in provider_info.get("body_omit", ()):
|
||||||
body.pop(field, None)
|
body.pop(field, None)
|
||||||
|
|
||||||
|
|
@ -1736,6 +1916,9 @@ class ExternalProviderClient:
|
||||||
compaction_threshold: Optional[int] = None,
|
compaction_threshold: Optional[int] = None,
|
||||||
tool_choice: Optional[Any] = 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,
|
fast_mode: Optional[bool] = None,
|
||||||
) -> AsyncGenerator[str, None]:
|
) -> AsyncGenerator[str, None]:
|
||||||
"""
|
"""
|
||||||
|
|
@ -2037,6 +2220,32 @@ class ExternalProviderClient:
|
||||||
body["temperature"] = temperature
|
body["temperature"] = temperature
|
||||||
if top_k is not None and top_k > 0 and not sampling_removed:
|
if top_k is not None and top_k > 0 and not sampling_removed:
|
||||||
body["top_k"] = top_k
|
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
|
# Anthropic only caches a prefix when at least one cache_control
|
||||||
# marker is attached to it — the frontend defaults
|
# marker is attached to it — the frontend defaults
|
||||||
# enable_prompt_caching to True for Anthropic, so treat `None` the
|
# enable_prompt_caching to True for Anthropic, so treat `None` the
|
||||||
|
|
@ -2232,6 +2441,16 @@ class ExternalProviderClient:
|
||||||
if anthropic_code_exec_container_id:
|
if anthropic_code_exec_container_id:
|
||||||
body["container"] = 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
|
# Server-side compaction (beta `compact-2026-01-12`). Clamps
|
||||||
# below-min thresholds to 50K so the request doesn't 400.
|
# below-min thresholds to 50K so the request doesn't 400.
|
||||||
# https://platform.claude.com/docs/en/build-with-claude/compaction
|
# https://platform.claude.com/docs/en/build-with-claude/compaction
|
||||||
|
|
@ -3235,6 +3454,8 @@ class ExternalProviderClient:
|
||||||
reasoning_effort: Optional[str] = None,
|
reasoning_effort: Optional[str] = None,
|
||||||
tools: Optional[list[dict[str, Any]]] = None,
|
tools: Optional[list[dict[str, Any]]] = None,
|
||||||
tool_choice: Optional[Any] = None,
|
tool_choice: Optional[Any] = None,
|
||||||
|
*,
|
||||||
|
stop: Optional[Union[str, list[str]]] = None,
|
||||||
) -> AsyncGenerator[str, None]:
|
) -> AsyncGenerator[str, None]:
|
||||||
"""
|
"""
|
||||||
Call Google's native Gemini API and translate its streaming
|
Call Google's native Gemini API and translate its streaming
|
||||||
|
|
@ -3928,6 +4149,14 @@ class ExternalProviderClient:
|
||||||
"thinkingBudget": thinking_budget,
|
"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:
|
if gen_config:
|
||||||
body["generationConfig"] = gen_config
|
body["generationConfig"] = gen_config
|
||||||
|
|
||||||
|
|
@ -4951,6 +5180,9 @@ class ExternalProviderClient:
|
||||||
compaction_threshold: Optional[int] = None,
|
compaction_threshold: Optional[int] = None,
|
||||||
tools: Optional[list[dict[str, Any]]] = None,
|
tools: Optional[list[dict[str, Any]]] = None,
|
||||||
tool_choice: Optional[Any] = None,
|
tool_choice: Optional[Any] = None,
|
||||||
|
*,
|
||||||
|
service_tier: Optional[str] = None,
|
||||||
|
parallel_tool_calls: Optional[bool] = None,
|
||||||
) -> AsyncGenerator[str, None]:
|
) -> AsyncGenerator[str, None]:
|
||||||
"""
|
"""
|
||||||
Call OpenAI's /v1/responses endpoint and translate its SSE stream back
|
Call OpenAI's /v1/responses endpoint and translate its SSE stream back
|
||||||
|
|
@ -5272,6 +5504,12 @@ class ExternalProviderClient:
|
||||||
"input": input_items,
|
"input": input_items,
|
||||||
"stream": True,
|
"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:
|
if previous_response_id:
|
||||||
body["previous_response_id"] = previous_response_id
|
body["previous_response_id"] = previous_response_id
|
||||||
# `summary: "auto"` is what makes /v1/responses emit reasoning
|
# `summary: "auto"` is what makes /v1/responses emit reasoning
|
||||||
|
|
|
||||||
|
|
@ -4247,6 +4247,36 @@ class LlamaCppBackend:
|
||||||
enable_thinking: Optional[bool] = None,
|
enable_thinking: Optional[bool] = None,
|
||||||
reasoning_effort: Optional[str] = None,
|
reasoning_effort: Optional[str] = None,
|
||||||
preserve_thinking: Optional[bool] = 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]:
|
) -> Generator[str | dict, None, None]:
|
||||||
"""
|
"""
|
||||||
Send a chat completion request to llama-server and stream tokens back.
|
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)
|
else (self._effective_context_length or _DEFAULT_MAX_TOKENS_FLOOR)
|
||||||
)
|
)
|
||||||
payload["t_max_predict_ms"] = _DEFAULT_T_MAX_PREDICT_MS
|
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:
|
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}
|
payload["stream_options"] = {"include_usage": True}
|
||||||
|
|
||||||
url = f"{self.base_url}/v1/chat/completions"
|
url = f"{self.base_url}/v1/chat/completions"
|
||||||
|
|
@ -4430,6 +4529,36 @@ class LlamaCppBackend:
|
||||||
auto_heal_tool_calls: bool = True,
|
auto_heal_tool_calls: bool = True,
|
||||||
tool_call_timeout: int = 300,
|
tool_call_timeout: int = 300,
|
||||||
session_id: Optional[str] = None,
|
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]:
|
) -> Generator[dict, None, None]:
|
||||||
"""
|
"""
|
||||||
Agentic loop: let the model call tools, execute them, and continue.
|
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)
|
else (self._effective_context_length or _DEFAULT_MAX_TOKENS_FLOOR)
|
||||||
)
|
)
|
||||||
payload["t_max_predict_ms"] = _DEFAULT_T_MAX_PREDICT_MS
|
payload["t_max_predict_ms"] = _DEFAULT_T_MAX_PREDICT_MS
|
||||||
|
# Same empty-string filter as the standard payload builder.
|
||||||
if stop:
|
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:
|
try:
|
||||||
_auth_headers = (
|
_auth_headers = (
|
||||||
|
|
@ -4996,6 +5192,12 @@ class LlamaCppBackend:
|
||||||
_accumulated_predicted_ms += _it.get("predicted_ms", 0)
|
_accumulated_predicted_ms += _it.get("predicted_ms", 0)
|
||||||
_accumulated_predicted_n += _it.get("predicted_n", 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}
|
assistant_msg = {"role": "assistant", "content": content_text}
|
||||||
if tool_calls:
|
if tool_calls:
|
||||||
assistant_msg["tool_calls"] = tool_calls
|
assistant_msg["tool_calls"] = tool_calls
|
||||||
|
|
@ -5198,8 +5400,78 @@ class LlamaCppBackend:
|
||||||
else (self._effective_context_length or _DEFAULT_MAX_TOKENS_FLOOR)
|
else (self._effective_context_length or _DEFAULT_MAX_TOKENS_FLOOR)
|
||||||
)
|
)
|
||||||
stream_payload["t_max_predict_ms"] = _DEFAULT_T_MAX_PREDICT_MS
|
stream_payload["t_max_predict_ms"] = _DEFAULT_T_MAX_PREDICT_MS
|
||||||
|
# Same empty-string filter as the standard / tool payload builder.
|
||||||
if stop:
|
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}
|
stream_payload["stream_options"] = {"include_usage": True}
|
||||||
|
|
||||||
cumulative = ""
|
cumulative = ""
|
||||||
|
|
|
||||||
|
|
@ -839,6 +839,7 @@ class InferenceOrchestrator:
|
||||||
tool_call_timeout: int = 300,
|
tool_call_timeout: int = 300,
|
||||||
session_id: Optional[str] = None,
|
session_id: Optional[str] = None,
|
||||||
use_adapter: Optional[Union[bool, str]] = None,
|
use_adapter: Optional[Union[bool, str]] = None,
|
||||||
|
parallel_tool_calls: Optional[bool] = None,
|
||||||
**_unused,
|
**_unused,
|
||||||
):
|
):
|
||||||
"""Run the safetensors agentic tool loop in this (parent)
|
"""Run the safetensors agentic tool loop in this (parent)
|
||||||
|
|
@ -895,6 +896,7 @@ class InferenceOrchestrator:
|
||||||
max_tool_iterations = max_tool_iterations,
|
max_tool_iterations = max_tool_iterations,
|
||||||
tool_call_timeout = tool_call_timeout,
|
tool_call_timeout = tool_call_timeout,
|
||||||
session_id = session_id,
|
session_id = session_id,
|
||||||
|
parallel_tool_calls = parallel_tool_calls,
|
||||||
)
|
)
|
||||||
|
|
||||||
def generate_with_adapter_control(
|
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-pro-latest|gemini-flash-latest|gemini-flash-lite-latest"
|
||||||
r")$"
|
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": {
|
"deepseek": {
|
||||||
"display_name": "DeepSeek",
|
"display_name": "DeepSeek",
|
||||||
"base_url": "https://api.deepseek.com/v1",
|
"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": [
|
"default_models": [
|
||||||
|
"deepseek-v4-pro",
|
||||||
|
"deepseek-v4-flash",
|
||||||
"deepseek-chat",
|
"deepseek-chat",
|
||||||
"deepseek-reasoner",
|
"deepseek-reasoner",
|
||||||
],
|
],
|
||||||
|
|
@ -150,7 +159,7 @@ PROVIDER_REGISTRY: dict[str, dict[str, Any]] = {
|
||||||
"supports_tool_calling": True,
|
"supports_tool_calling": True,
|
||||||
"auth_header": "Authorization",
|
"auth_header": "Authorization",
|
||||||
"auth_prefix": "Bearer ",
|
"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": {
|
"mistral": {
|
||||||
"display_name": "Mistral AI",
|
"display_name": "Mistral AI",
|
||||||
|
|
@ -180,6 +189,12 @@ PROVIDER_REGISTRY: dict[str, dict[str, Any]] = {
|
||||||
r"mistral-(?:large|medium|small|tiny)-latest|"
|
r"mistral-(?:large|medium|small|tiny)-latest|"
|
||||||
r"mistral-vibe-cli-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": {
|
"kimi": {
|
||||||
"display_name": "Kimi",
|
"display_name": "Kimi",
|
||||||
|
|
@ -203,11 +218,21 @@ PROVIDER_REGISTRY: dict[str, dict[str, Any]] = {
|
||||||
"auth_prefix": "Bearer ",
|
"auth_prefix": "Bearer ",
|
||||||
"notes": "Moonshot API key. China: use base URL https://api.moonshot.cn/v1",
|
"notes": "Moonshot API key. China: use base URL https://api.moonshot.cn/v1",
|
||||||
"model_id_allowlist": re.compile(r"^kimi-k2\.[56]$"),
|
"model_id_allowlist": re.compile(r"^kimi-k2\.[56]$"),
|
||||||
# Both k2.6 and k2.5 are reasoning-class. The API rejects custom
|
# k2.5/k2.6 are reasoning-class: API locks temperature=1, top_p=1,
|
||||||
# sampling: "invalid temperature: only 1 is allowed for this model"
|
# frequency_penalty; presence_penalty / seed / parallel_tool_calls
|
||||||
# (and the same shape for top_p). Strip both fields from the
|
# are undocumented in the Kimi chat schema.
|
||||||
# outbound body so the server falls back to its required defaults.
|
"body_omit": (
|
||||||
"body_omit": ("temperature", "top_p"),
|
"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": {
|
"qwen": {
|
||||||
"display_name": "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.",
|
"notes": "Unified gateway to 300+ models across all major providers. HTTP-Referer and X-Title headers sent for attribution.",
|
||||||
"model_list_mode": "curated",
|
"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,
|
max_tool_iterations: int = 25,
|
||||||
tool_call_timeout: int = 300,
|
tool_call_timeout: int = 300,
|
||||||
session_id: Optional[str] = None,
|
session_id: Optional[str] = None,
|
||||||
|
parallel_tool_calls: Optional[bool] = None,
|
||||||
) -> Generator[dict, None, None]:
|
) -> Generator[dict, None, None]:
|
||||||
"""Drive an agentic tool loop on top of a cumulative-text generator.
|
"""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": ""}
|
yield {"type": "status", "text": ""}
|
||||||
return
|
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}
|
assistant_msg: dict = {"role": "assistant", "content": content_text}
|
||||||
if tool_calls:
|
if tool_calls:
|
||||||
assistant_msg["tool_calls"] = tool_calls
|
assistant_msg["tool_calls"] = tool_calls
|
||||||
|
|
|
||||||
|
|
@ -863,13 +863,187 @@ class ChatCompletionRequest(BaseModel):
|
||||||
"to auto-create."
|
"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(
|
fast_mode: Optional[bool] = Field(
|
||||||
None,
|
None,
|
||||||
description = (
|
description = (
|
||||||
"[x-unsloth] Anthropic fast-mode toggle. On Claude Opus 4.6 / "
|
"[x-unsloth] Anthropic fast-mode on Opus 4.6 / 4.7. Adds the "
|
||||||
"4.7 adds the `fast-mode-2026-02-01` beta header and sends "
|
"fast-mode-2026-02-01 beta header + speed:'fast' for higher "
|
||||||
"`speed: 'fast'` for higher OTPS at premium pricing. Silently "
|
"OTPS at premium pricing. Silently dropped elsewhere. "
|
||||||
"ignored on every other model + provider. See "
|
|
||||||
"https://platform.claude.com/docs/en/build-with-claude/fast-mode"
|
"https://platform.claude.com/docs/en/build-with-claude/fast-mode"
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
|
|
|
||||||
|
|
@ -99,6 +99,9 @@ class ChatExportResponse(BaseModel):
|
||||||
|
|
||||||
|
|
||||||
class ChatInferenceSettings(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")
|
model_config = ConfigDict(extra = "forbid")
|
||||||
|
|
||||||
temperature: Optional[float] = None
|
temperature: Optional[float] = None
|
||||||
|
|
@ -107,10 +110,47 @@ class ChatInferenceSettings(BaseModel):
|
||||||
minP: Optional[float] = None
|
minP: Optional[float] = None
|
||||||
repetitionPenalty: Optional[float] = None
|
repetitionPenalty: Optional[float] = None
|
||||||
presencePenalty: 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
|
maxSeqLength: Optional[float] = None
|
||||||
maxTokens: Optional[float] = None
|
maxTokens: Optional[float] = None
|
||||||
systemPrompt: Optional[str] = None
|
systemPrompt: Optional[str] = None
|
||||||
trustRemoteCode: Optional[bool] = 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):
|
class ChatPreset(BaseModel):
|
||||||
|
|
|
||||||
|
|
@ -239,6 +239,19 @@ router = APIRouter()
|
||||||
studio_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:
|
def _detect_safetensors_features(backend, chat_template: Optional[str]) -> dict:
|
||||||
"""Classify reasoning/tool capabilities via the GGUF classifier so
|
"""Classify reasoning/tool capabilities via the GGUF classifier so
|
||||||
flags match across backends. gpt-oss is overridden because Harmony
|
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,
|
anthropic_code_exec_container_id = payload.anthropic_code_exec_container_id,
|
||||||
prompt_cache_ttl = payload.prompt_cache_ttl,
|
prompt_cache_ttl = payload.prompt_cache_ttl,
|
||||||
compaction_threshold = payload.compaction_threshold,
|
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,
|
tools = payload.tools,
|
||||||
tool_choice = payload.tool_choice,
|
tool_choice = payload.tool_choice,
|
||||||
fast_mode = payload.fast_mode,
|
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,
|
stream = payload.stream,
|
||||||
)
|
)
|
||||||
try:
|
try:
|
||||||
|
|
@ -2776,6 +2822,7 @@ async def openai_chat_completions(
|
||||||
max_tokens = payload.max_tokens,
|
max_tokens = payload.max_tokens,
|
||||||
repetition_penalty = payload.repetition_penalty,
|
repetition_penalty = payload.repetition_penalty,
|
||||||
presence_penalty = payload.presence_penalty,
|
presence_penalty = payload.presence_penalty,
|
||||||
|
stop = _clean_local_stop_list(payload.stop),
|
||||||
cancel_event = cancel_event,
|
cancel_event = cancel_event,
|
||||||
enable_thinking = payload.enable_thinking,
|
enable_thinking = payload.enable_thinking,
|
||||||
reasoning_effort = payload.reasoning_effort,
|
reasoning_effort = payload.reasoning_effort,
|
||||||
|
|
@ -2790,6 +2837,36 @@ async def openai_chat_completions(
|
||||||
if payload.tool_call_timeout is not None
|
if payload.tool_call_timeout is not None
|
||||||
else 300,
|
else 300,
|
||||||
session_id = payload.session_id,
|
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()
|
_tool_sentinel = object()
|
||||||
|
|
@ -2955,10 +3032,41 @@ async def openai_chat_completions(
|
||||||
max_tokens = payload.max_tokens,
|
max_tokens = payload.max_tokens,
|
||||||
repetition_penalty = payload.repetition_penalty,
|
repetition_penalty = payload.repetition_penalty,
|
||||||
presence_penalty = payload.presence_penalty,
|
presence_penalty = payload.presence_penalty,
|
||||||
|
stop = _clean_local_stop_list(payload.stop),
|
||||||
cancel_event = cancel_event,
|
cancel_event = cancel_event,
|
||||||
enable_thinking = payload.enable_thinking,
|
enable_thinking = payload.enable_thinking,
|
||||||
reasoning_effort = payload.reasoning_effort,
|
reasoning_effort = payload.reasoning_effort,
|
||||||
preserve_thinking = payload.preserve_thinking,
|
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()
|
_gguf_sentinel = object()
|
||||||
|
|
@ -3298,6 +3406,7 @@ async def openai_chat_completions(
|
||||||
else 300,
|
else 300,
|
||||||
session_id = payload.session_id,
|
session_id = payload.session_id,
|
||||||
use_adapter = payload.use_adapter,
|
use_adapter = payload.use_adapter,
|
||||||
|
parallel_tool_calls = payload.parallel_tool_calls,
|
||||||
)
|
)
|
||||||
|
|
||||||
_sf_tool_sentinel = object()
|
_sf_tool_sentinel = object()
|
||||||
|
|
@ -4101,6 +4210,9 @@ def _build_chat_request(
|
||||||
chat_kwargs["top_p"] = payload.top_p
|
chat_kwargs["top_p"] = payload.top_p
|
||||||
if payload.max_output_tokens is not None:
|
if payload.max_output_tokens is not None:
|
||||||
chat_kwargs["max_tokens"] = payload.max_output_tokens
|
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)
|
chat_tools = _translate_responses_tools_to_chat(payload.tools)
|
||||||
if chat_tools is not None:
|
if chat_tools is not None:
|
||||||
|
|
@ -4110,13 +4222,7 @@ def _build_chat_request(
|
||||||
if chat_tool_choice is not None:
|
if chat_tool_choice is not None:
|
||||||
chat_kwargs["tool_choice"] = chat_tool_choice
|
chat_kwargs["tool_choice"] = chat_tool_choice
|
||||||
|
|
||||||
req = ChatCompletionRequest(**chat_kwargs)
|
return 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
|
|
||||||
|
|
||||||
|
|
||||||
def _chat_tool_calls_to_responses_output(tool_calls: list[dict]) -> list[dict]:
|
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 {}
|
msg = choices[0].get("message", {}) or {}
|
||||||
text = msg.get("content", "") or ""
|
text = msg.get("content", "") or ""
|
||||||
tool_calls = msg.get("tool_calls") 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", {})
|
usage_data = body.get("usage", {})
|
||||||
input_tokens = usage_data.get("prompt_tokens", 0)
|
input_tokens = usage_data.get("prompt_tokens", 0)
|
||||||
|
|
@ -4288,6 +4398,11 @@ async def _responses_stream(
|
||||||
tool_call_state: dict[int, dict] = {}
|
tool_call_state: dict[int, dict] = {}
|
||||||
# Text message lives at output_index 0; tool calls claim 1, 2, ...
|
# Text message lives at output_index 0; tool calls claim 1, 2, ...
|
||||||
next_output_index = 1
|
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]:
|
def _snapshot_output() -> list[dict]:
|
||||||
"""Snapshot of all completed output items for response.completed."""
|
"""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 []:
|
for tc in delta.get("tool_calls") or []:
|
||||||
idx = tc.get("index", 0)
|
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)
|
st = tool_call_state.get(idx)
|
||||||
fn = tc.get("function") or {}
|
fn = tc.get("function") or {}
|
||||||
if st is None:
|
if st is None:
|
||||||
|
|
@ -4773,6 +4893,15 @@ async def anthropic_messages(
|
||||||
if openai_tool_choice is None:
|
if openai_tool_choice is None:
|
||||||
openai_tool_choice = "auto"
|
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()
|
cancel_event = threading.Event()
|
||||||
|
|
||||||
# ── Tool routing ──────────────────────────────────────────
|
# ── Tool routing ──────────────────────────────────────────
|
||||||
|
|
@ -4869,6 +4998,7 @@ async def anthropic_messages(
|
||||||
repetition_penalty = repetition_penalty,
|
repetition_penalty = repetition_penalty,
|
||||||
presence_penalty = presence_penalty,
|
presence_penalty = presence_penalty,
|
||||||
tool_choice = openai_tool_choice,
|
tool_choice = openai_tool_choice,
|
||||||
|
parallel_tool_calls = anthropic_parallel_tool_calls,
|
||||||
session_id = payload.session_id,
|
session_id = payload.session_id,
|
||||||
cancel_id = payload.cancel_id,
|
cancel_id = payload.cancel_id,
|
||||||
)
|
)
|
||||||
|
|
@ -4887,6 +5017,7 @@ async def anthropic_messages(
|
||||||
repetition_penalty = repetition_penalty,
|
repetition_penalty = repetition_penalty,
|
||||||
presence_penalty = presence_penalty,
|
presence_penalty = presence_penalty,
|
||||||
tool_choice = openai_tool_choice,
|
tool_choice = openai_tool_choice,
|
||||||
|
parallel_tool_calls = anthropic_parallel_tool_calls,
|
||||||
)
|
)
|
||||||
|
|
||||||
if server_tools:
|
if server_tools:
|
||||||
|
|
@ -4978,6 +5109,7 @@ async def anthropic_messages(
|
||||||
auto_heal_tool_calls = True,
|
auto_heal_tool_calls = True,
|
||||||
tool_call_timeout = 300,
|
tool_call_timeout = 300,
|
||||||
session_id = payload.session_id,
|
session_id = payload.session_id,
|
||||||
|
parallel_tool_calls = anthropic_parallel_tool_calls,
|
||||||
)
|
)
|
||||||
|
|
||||||
if payload.stream:
|
if payload.stream:
|
||||||
|
|
@ -5229,6 +5361,36 @@ def _build_passthrough_payload(
|
||||||
min_p = None,
|
min_p = None,
|
||||||
repetition_penalty = None,
|
repetition_penalty = None,
|
||||||
presence_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",
|
tool_choice = "auto",
|
||||||
response_format = None,
|
response_format = None,
|
||||||
chat_template_kwargs = None,
|
chat_template_kwargs = None,
|
||||||
|
|
@ -5251,8 +5413,16 @@ def _build_passthrough_payload(
|
||||||
else (backend_ctx or _DEFAULT_MAX_TOKENS_FLOOR)
|
else (backend_ctx or _DEFAULT_MAX_TOKENS_FLOOR)
|
||||||
)
|
)
|
||||||
body["t_max_predict_ms"] = _DEFAULT_T_MAX_PREDICT_MS
|
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:
|
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:
|
if min_p is not None:
|
||||||
body["min_p"] = min_p
|
body["min_p"] = min_p
|
||||||
if repetition_penalty is not None:
|
if repetition_penalty is not None:
|
||||||
|
|
@ -5260,16 +5430,76 @@ def _build_passthrough_payload(
|
||||||
body["repeat_penalty"] = repetition_penalty
|
body["repeat_penalty"] = repetition_penalty
|
||||||
if presence_penalty is not None:
|
if presence_penalty is not None:
|
||||||
body["presence_penalty"] = presence_penalty
|
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:
|
if response_format is not None:
|
||||||
# llama-server applies a GBNF grammar derived from the JSON schema
|
# llama-server applies a GBNF grammar from the JSON schema.
|
||||||
# when response_format is present. Field is documented flat at the
|
# Field is documented flat at the request root.
|
||||||
# request root (tools/server/README.md), which is also what the
|
|
||||||
# OpenAI SDK produces by spreading extra_body into the body top.
|
|
||||||
body["response_format"] = response_format
|
body["response_format"] = response_format
|
||||||
if chat_template_kwargs is not None:
|
if chat_template_kwargs is not None:
|
||||||
# Propagate reasoning / template overrides (e.g. enable_thinking)
|
# Reasoning / template overrides (e.g. enable_thinking) so
|
||||||
# so llama-server renders the Jinja template in the mode the caller
|
# llama-server renders the Jinja template in the requested mode.
|
||||||
# asked for instead of whatever default the model was loaded with.
|
|
||||||
body["chat_template_kwargs"] = chat_template_kwargs
|
body["chat_template_kwargs"] = chat_template_kwargs
|
||||||
return body
|
return body
|
||||||
|
|
||||||
|
|
@ -5291,6 +5521,7 @@ async def _anthropic_passthrough_stream(
|
||||||
repetition_penalty = None,
|
repetition_penalty = None,
|
||||||
presence_penalty = None,
|
presence_penalty = None,
|
||||||
tool_choice = "auto",
|
tool_choice = "auto",
|
||||||
|
parallel_tool_calls = None,
|
||||||
session_id = None,
|
session_id = None,
|
||||||
cancel_id = None,
|
cancel_id = None,
|
||||||
):
|
):
|
||||||
|
|
@ -5309,6 +5540,7 @@ async def _anthropic_passthrough_stream(
|
||||||
min_p = min_p,
|
min_p = min_p,
|
||||||
repetition_penalty = repetition_penalty,
|
repetition_penalty = repetition_penalty,
|
||||||
presence_penalty = presence_penalty,
|
presence_penalty = presence_penalty,
|
||||||
|
parallel_tool_calls = parallel_tool_calls,
|
||||||
tool_choice = tool_choice,
|
tool_choice = tool_choice,
|
||||||
backend_ctx = llama_backend.context_length,
|
backend_ctx = llama_backend.context_length,
|
||||||
)
|
)
|
||||||
|
|
@ -5319,7 +5551,9 @@ async def _anthropic_passthrough_stream(
|
||||||
_tracker.__enter__()
|
_tracker.__enter__()
|
||||||
|
|
||||||
async def _stream():
|
async def _stream():
|
||||||
emitter = AnthropicPassthroughEmitter()
|
emitter = AnthropicPassthroughEmitter(
|
||||||
|
parallel_tool_calls = parallel_tool_calls,
|
||||||
|
)
|
||||||
for line in emitter.start(message_id, model_name):
|
for line in emitter.start(message_id, model_name):
|
||||||
yield line
|
yield line
|
||||||
|
|
||||||
|
|
@ -5443,6 +5677,7 @@ async def _anthropic_passthrough_non_streaming(
|
||||||
repetition_penalty = None,
|
repetition_penalty = None,
|
||||||
presence_penalty = None,
|
presence_penalty = None,
|
||||||
tool_choice = "auto",
|
tool_choice = "auto",
|
||||||
|
parallel_tool_calls = None,
|
||||||
):
|
):
|
||||||
"""Non-streaming client-side pass-through."""
|
"""Non-streaming client-side pass-through."""
|
||||||
target_url = f"{llama_backend.base_url}/v1/chat/completions"
|
target_url = f"{llama_backend.base_url}/v1/chat/completions"
|
||||||
|
|
@ -5458,6 +5693,7 @@ async def _anthropic_passthrough_non_streaming(
|
||||||
min_p = min_p,
|
min_p = min_p,
|
||||||
repetition_penalty = repetition_penalty,
|
repetition_penalty = repetition_penalty,
|
||||||
presence_penalty = presence_penalty,
|
presence_penalty = presence_penalty,
|
||||||
|
parallel_tool_calls = parallel_tool_calls,
|
||||||
tool_choice = tool_choice,
|
tool_choice = tool_choice,
|
||||||
backend_ctx = llama_backend.context_length,
|
backend_ctx = llama_backend.context_length,
|
||||||
)
|
)
|
||||||
|
|
@ -5484,6 +5720,10 @@ async def _anthropic_passthrough_non_streaming(
|
||||||
content_blocks.append(AnthropicResponseTextBlock(text = text))
|
content_blocks.append(AnthropicResponseTextBlock(text = text))
|
||||||
|
|
||||||
tool_calls = message.get("tool_calls") or []
|
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:
|
for tc in tool_calls:
|
||||||
fn = tc.get("function") or {}
|
fn = tc.get("function") or {}
|
||||||
try:
|
try:
|
||||||
|
|
@ -5786,6 +6026,36 @@ def _build_openai_passthrough_body(payload, backend_ctx = None) -> dict:
|
||||||
min_p = payload.min_p,
|
min_p = payload.min_p,
|
||||||
repetition_penalty = payload.repetition_penalty,
|
repetition_penalty = payload.repetition_penalty,
|
||||||
presence_penalty = payload.presence_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,
|
tool_choice = tool_choice,
|
||||||
response_format = _extract_response_format(payload),
|
response_format = _extract_response_format(payload),
|
||||||
chat_template_kwargs = tpl_kwargs,
|
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.
|
# 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):
|
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})
|
configure_cpu_threads({"UNSLOTH_CPU_THREADS": raw})
|
||||||
|
|
||||||
|
|
||||||
# env=None path uses real os.environ (production call from run.py / main.py).
|
# 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):
|
def test_cpu_thread_cap_uses_os_environ_when_env_is_none(monkeypatch):
|
||||||
for variable in (*_THREAD_POOL_ENV_VARS, "UNSLOTH_CPU_THREADS"):
|
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")
|
monkeypatch.setenv("UNSLOTH_CPU_THREADS", "3")
|
||||||
|
|
||||||
configure_cpu_threads()
|
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.
|
# Calling twice must not flip any seeded value.
|
||||||
def test_cpu_thread_cap_idempotent(monkeypatch):
|
def test_cpu_thread_cap_idempotent(monkeypatch):
|
||||||
for variable in (*_THREAD_POOL_ENV_VARS, "UNSLOTH_CPU_THREADS"):
|
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")
|
monkeypatch.setenv("UNSLOTH_CPU_THREADS", "5")
|
||||||
|
|
||||||
configure_cpu_threads()
|
configure_cpu_threads()
|
||||||
|
|
@ -138,9 +140,9 @@ def test_invalid_cpu_thread_cap_exits_without_traceback(entry_point):
|
||||||
|
|
||||||
result = subprocess.run(
|
result = subprocess.run(
|
||||||
[sys.executable, str(entry_point)],
|
[sys.executable, str(entry_point)],
|
||||||
env=env,
|
env = env,
|
||||||
capture_output=True,
|
capture_output = True,
|
||||||
text=True,
|
text = True,
|
||||||
)
|
)
|
||||||
|
|
||||||
assert result.returncode == 1
|
assert result.returncode == 1
|
||||||
|
|
|
||||||
|
|
@ -275,18 +275,19 @@ class TestChatCompletionRequestToolFields:
|
||||||
assert req.stop is None
|
assert req.stop is None
|
||||||
|
|
||||||
def test_extra_fields_accepted(self):
|
def test_extra_fields_accepted(self):
|
||||||
# `frequency_penalty`, `seed`, `response_format` are not yet
|
# ``response_format`` is still an undeclared OpenAI-side field;
|
||||||
# explicitly declared but must survive Pydantic parsing now that
|
# it must survive Pydantic parsing because extra="allow" is set.
|
||||||
# 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(
|
req = self._make(
|
||||||
frequency_penalty = 0.5,
|
frequency_penalty = 0.5,
|
||||||
seed = 42,
|
seed = 42,
|
||||||
response_format = {"type": "json_object"},
|
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 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"}
|
assert req.model_extra.get("response_format") == {"type": "json_object"}
|
||||||
|
|
||||||
def test_unsloth_extensions_still_work(self):
|
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(
|
const externalCapabilities = getProviderCapabilities(
|
||||||
externalProvider?.providerType,
|
externalProvider?.providerType,
|
||||||
|
externalSelection?.modelId,
|
||||||
);
|
);
|
||||||
const externalReasoningCaps: ReturnType<
|
const externalReasoningCaps: ReturnType<
|
||||||
typeof getExternalReasoningCapabilities
|
typeof getExternalReasoningCapabilities
|
||||||
|
|
@ -1990,6 +1991,166 @@ export function createOpenAIStreamAdapter(): ChatModelAdapter {
|
||||||
...(externalCapabilities?.presencePenalty
|
...(externalCapabilities?.presencePenalty
|
||||||
? { presence_penalty: params.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;
|
// Compose the enabled_tools list from the active pills;
|
||||||
// backend maps each name to the provider's tool schema.
|
// backend maps each name to the provider's tool schema.
|
||||||
...(webSearchEnabledForThisTurn ||
|
...(webSearchEnabledForThisTurn ||
|
||||||
|
|
@ -2045,9 +2206,8 @@ export function createOpenAIStreamAdapter(): ChatModelAdapter {
|
||||||
isPromptCacheTtl(externalProvider.promptCacheTtl)
|
isPromptCacheTtl(externalProvider.promptCacheTtl)
|
||||||
? { prompt_cache_ttl: externalProvider.promptCacheTtl }
|
? { prompt_cache_ttl: externalProvider.promptCacheTtl }
|
||||||
: {}),
|
: {}),
|
||||||
// Anthropic fast mode (Opus 4.6 / 4.7 only); backend
|
// Fast mode (Anthropic Opus 4.6 / 4.7). Backend drops on
|
||||||
// silently drops on unsupported models as a second
|
// unsupported models as second defence.
|
||||||
// line of defence.
|
|
||||||
...(params.fastMode &&
|
...(params.fastMode &&
|
||||||
providerSupportsFastMode(
|
providerSupportsFastMode(
|
||||||
externalProvider.providerType,
|
externalProvider.providerType,
|
||||||
|
|
@ -2080,6 +2240,107 @@ export function createOpenAIStreamAdapter(): ChatModelAdapter {
|
||||||
min_p: params.minP,
|
min_p: params.minP,
|
||||||
repetition_penalty: params.repetitionPenalty,
|
repetition_penalty: params.repetitionPenalty,
|
||||||
presence_penalty: params.presencePenalty,
|
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,
|
image_base64: imageBase64,
|
||||||
audio_base64: audioBase64,
|
audio_base64: audioBase64,
|
||||||
cancel_id: cancelId,
|
cancel_id: cancelId,
|
||||||
|
|
|
||||||
|
|
@ -698,7 +698,10 @@ export function ChatPage(): ReactElement {
|
||||||
const provider = externalProvidersForChat.find(
|
const provider = externalProvidersForChat.find(
|
||||||
(p) => p.id === selection.providerId,
|
(p) => p.id === selection.providerId,
|
||||||
);
|
);
|
||||||
const baseCapabilities = getProviderCapabilities(provider?.providerType);
|
const baseCapabilities = getProviderCapabilities(
|
||||||
|
provider?.providerType,
|
||||||
|
selection.modelId,
|
||||||
|
);
|
||||||
if (!baseCapabilities) return baseCapabilities;
|
if (!baseCapabilities) return baseCapabilities;
|
||||||
const anthropicThinkingEnabled =
|
const anthropicThinkingEnabled =
|
||||||
provider?.providerType === "anthropic" &&
|
provider?.providerType === "anthropic" &&
|
||||||
|
|
|
||||||
|
|
@ -87,13 +87,17 @@ import {
|
||||||
type ProviderCapabilities,
|
type ProviderCapabilities,
|
||||||
getExternalMaxOutputTokens,
|
getExternalMaxOutputTokens,
|
||||||
getExternalMinOutputTokens,
|
getExternalMinOutputTokens,
|
||||||
|
getProviderStopMax,
|
||||||
|
getServiceTierOptions,
|
||||||
providerSupportsBuiltinCodeExecution,
|
providerSupportsBuiltinCodeExecution,
|
||||||
providerSupportsFastMode,
|
providerSupportsFastMode,
|
||||||
} from "./provider-capabilities";
|
} from "./provider-capabilities";
|
||||||
|
import { StopSequencesInput } from "@/components/ui/stop-sequences-input";
|
||||||
import { useChatRuntimeStore } from "./stores/chat-runtime-store";
|
import { useChatRuntimeStore } from "./stores/chat-runtime-store";
|
||||||
import { ChatMcpServersDialog } from "./chat-mcp-servers-dialog";
|
import { ChatMcpServersDialog } from "./chat-mcp-servers-dialog";
|
||||||
import { listMcpServers } from "./api/mcp-servers-api";
|
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 { defaultInferenceParams, type Preset } from "./presets/preset-policy";
|
||||||
export type { InferenceParams } from "./types/runtime";
|
export type { InferenceParams } from "./types/runtime";
|
||||||
|
|
@ -129,21 +133,11 @@ export function InfoHint({ children }: { children: ReactNode }) {
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/** Editable numeric value display: transparent text-like input that
|
||||||
* Editable numeric value display.
|
* shows formatted display on blur (so "Off"/"Max" labels render) and
|
||||||
*
|
* switches to the raw number on focus. Commits on blur/Enter, reverts
|
||||||
* Renders as a single <input> that *looks* like text by default —
|
* on Escape, clamps on commit. Shared by every slider value + the
|
||||||
* transparent background, no border, no ring — and only shows a faint
|
* Context Length input. */
|
||||||
* 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.
|
|
||||||
*/
|
|
||||||
function snapToStep(
|
function snapToStep(
|
||||||
value: number,
|
value: number,
|
||||||
step: number,
|
step: number,
|
||||||
|
|
@ -407,10 +401,14 @@ export function ChatSettingsPanel({
|
||||||
externalProviderType = null,
|
externalProviderType = null,
|
||||||
onReloadModel,
|
onReloadModel,
|
||||||
}: ChatSettingsPanelProps) {
|
}: ChatSettingsPanelProps) {
|
||||||
// For non-external (local) models we show every knob — providerCapabilities
|
const isMobile = useIsMobile();
|
||||||
// is only consulted when `isExternalModel` is true. An external model with an
|
const isGguf = useChatRuntimeStore((s) => s.activeGgufVariant) != null;
|
||||||
// unknown provider falls back to the OpenAI-compat shape via
|
// Local models show every knob (providerCapabilities only consulted
|
||||||
// getProviderCapabilities, so these flags never undercount support.
|
// 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 =
|
const showTemperature =
|
||||||
!isExternalModel || Boolean(providerCapabilities?.temperature);
|
!isExternalModel || Boolean(providerCapabilities?.temperature);
|
||||||
const showTopP = !isExternalModel || Boolean(providerCapabilities?.topP);
|
const showTopP = !isExternalModel || Boolean(providerCapabilities?.topP);
|
||||||
|
|
@ -420,8 +418,60 @@ export function ChatSettingsPanel({
|
||||||
!isExternalModel || Boolean(providerCapabilities?.repetitionPenalty);
|
!isExternalModel || Boolean(providerCapabilities?.repetitionPenalty);
|
||||||
const showPresencePenalty =
|
const showPresencePenalty =
|
||||||
!isExternalModel || Boolean(providerCapabilities?.presencePenalty);
|
!isExternalModel || Boolean(providerCapabilities?.presencePenalty);
|
||||||
const isMobile = useIsMobile();
|
const showFrequencyPenalty = isExternalModel
|
||||||
const isGguf = useChatRuntimeStore((s) => s.activeGgufVariant) != null;
|
? 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 =
|
const hasModelContent =
|
||||||
!isExternalModel && (isGguf || Boolean(params.checkpoint));
|
!isExternalModel && (isGguf || Boolean(params.checkpoint));
|
||||||
const speculativeType = useChatRuntimeStore((s) => s.speculativeType);
|
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."
|
info="Penalizes any token that has already appeared at least once, encouraging the model to introduce new topics. 0 = off."
|
||||||
/>
|
/>
|
||||||
) : null}
|
) : 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 && (
|
{!isExternalModel && !isGguf && (
|
||||||
<ParamSlider
|
<ParamSlider
|
||||||
label="Max Seq Length"
|
label="Max Seq Length"
|
||||||
|
|
@ -1299,7 +1486,7 @@ export function ChatSettingsPanel({
|
||||||
max={32768}
|
max={32768}
|
||||||
step={128}
|
step={128}
|
||||||
onChange={set("maxSeqLength")}
|
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
|
<ParamSlider
|
||||||
|
|
@ -1334,6 +1521,503 @@ export function ChatSettingsPanel({
|
||||||
</div>
|
</div>
|
||||||
</CollapsibleSection>
|
</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 ? (
|
{!isExternalModel ? (
|
||||||
<CollapsibleSection label="Tools">
|
<CollapsibleSection label="Tools">
|
||||||
<div className="flex flex-col gap-5 pt-1">
|
<div className="flex flex-col gap-5 pt-1">
|
||||||
|
|
|
||||||
|
|
@ -13,6 +13,11 @@ export interface Preset {
|
||||||
params: InferenceParams;
|
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<
|
export type PresetOwnedParams = Pick<
|
||||||
InferenceParams,
|
InferenceParams,
|
||||||
| "temperature"
|
| "temperature"
|
||||||
|
|
@ -21,6 +26,8 @@ export type PresetOwnedParams = Pick<
|
||||||
| "minP"
|
| "minP"
|
||||||
| "repetitionPenalty"
|
| "repetitionPenalty"
|
||||||
| "presencePenalty"
|
| "presencePenalty"
|
||||||
|
| "frequencyPenalty"
|
||||||
|
| "stop"
|
||||||
| "maxTokens"
|
| "maxTokens"
|
||||||
| "systemPrompt"
|
| "systemPrompt"
|
||||||
>;
|
>;
|
||||||
|
|
@ -103,11 +110,22 @@ export function getPresetOwnedParams(
|
||||||
minP: params.minP,
|
minP: params.minP,
|
||||||
repetitionPenalty: params.repetitionPenalty,
|
repetitionPenalty: params.repetitionPenalty,
|
||||||
presencePenalty: params.presencePenalty,
|
presencePenalty: params.presencePenalty,
|
||||||
|
frequencyPenalty: params.frequencyPenalty,
|
||||||
|
stop: params.stop,
|
||||||
maxTokens: params.maxTokens,
|
maxTokens: params.maxTokens,
|
||||||
systemPrompt: params.systemPrompt,
|
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(
|
export function isSamePresetConfig(
|
||||||
a: InferenceParams,
|
a: InferenceParams,
|
||||||
b: InferenceParams,
|
b: InferenceParams,
|
||||||
|
|
@ -121,6 +139,8 @@ export function isSamePresetConfig(
|
||||||
left.minP === right.minP &&
|
left.minP === right.minP &&
|
||||||
left.repetitionPenalty === right.repetitionPenalty &&
|
left.repetitionPenalty === right.repetitionPenalty &&
|
||||||
left.presencePenalty === right.presencePenalty &&
|
left.presencePenalty === right.presencePenalty &&
|
||||||
|
left.frequencyPenalty === right.frequencyPenalty &&
|
||||||
|
stopArraysEqual(left.stop, right.stop) &&
|
||||||
left.maxTokens === right.maxTokens &&
|
left.maxTokens === right.maxTokens &&
|
||||||
left.systemPrompt === right.systemPrompt
|
left.systemPrompt === right.systemPrompt
|
||||||
);
|
);
|
||||||
|
|
|
||||||
File diff suppressed because it is too large
Load diff
|
|
@ -406,11 +406,45 @@ const PERSISTED_INFERENCE_PARAM_KEYS = [
|
||||||
"minP",
|
"minP",
|
||||||
"repetitionPenalty",
|
"repetitionPenalty",
|
||||||
"presencePenalty",
|
"presencePenalty",
|
||||||
|
"frequencyPenalty",
|
||||||
|
"seed",
|
||||||
|
"stop",
|
||||||
|
"serviceTier",
|
||||||
|
"parallelToolCalls",
|
||||||
"maxSeqLength",
|
"maxSeqLength",
|
||||||
"maxTokens",
|
"maxTokens",
|
||||||
"systemPrompt",
|
"systemPrompt",
|
||||||
"trustRemoteCode",
|
"trustRemoteCode",
|
||||||
"fastMode",
|
"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[];
|
] as const satisfies readonly PersistedInferenceParamKey[];
|
||||||
|
|
||||||
const SCALAR_SETTING_KEYS = [
|
const SCALAR_SETTING_KEYS = [
|
||||||
|
|
|
||||||
|
|
@ -314,12 +314,81 @@ export interface OpenAIChatCompletionsRequest {
|
||||||
* the Anthropic provider with `code_execution` in `enabled_tools`.
|
* the Anthropic provider with `code_execution` in `enabled_tools`.
|
||||||
*/
|
*/
|
||||||
anthropic_code_exec_container_id?: string | null;
|
anthropic_code_exec_container_id?: string | null;
|
||||||
/**
|
/** OpenAI Chat only. Range -2..2. */
|
||||||
* Anthropic fast-mode toggle. Opus 4.6 / 4.7 only; backend drops
|
frequency_penalty?: number;
|
||||||
* silently on every other model + provider. See
|
/** OAI Chat + most OAI-compat. Responses + Anthropic drop. */
|
||||||
* https://platform.claude.com/docs/en/build-with-claude/fast-mode
|
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;
|
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 {
|
export interface OpenAIChatDelta {
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,16 @@
|
||||||
// SPDX-License-Identifier: AGPL-3.0-only
|
// SPDX-License-Identifier: AGPL-3.0-only
|
||||||
// Copyright 2026-present the Unsloth AI Inc. team. All rights reserved. See /studio/LICENSE.AGPL-3.0
|
// 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 {
|
export interface InferenceParams {
|
||||||
temperature: number;
|
temperature: number;
|
||||||
topP: number;
|
topP: number;
|
||||||
|
|
@ -8,17 +18,77 @@ export interface InferenceParams {
|
||||||
minP: number;
|
minP: number;
|
||||||
repetitionPenalty: number;
|
repetitionPenalty: number;
|
||||||
presencePenalty: 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;
|
maxSeqLength: number;
|
||||||
maxTokens: number;
|
maxTokens: number;
|
||||||
systemPrompt: string;
|
systemPrompt: string;
|
||||||
checkpoint: 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;
|
trustRemoteCode?: boolean;
|
||||||
/**
|
/** Anthropic Opus 4.6 / 4.7 only. 6x pricing for higher OTPS. */
|
||||||
* 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
|
|
||||||
*/
|
|
||||||
fastMode?: boolean;
|
fastMode?: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -29,6 +99,39 @@ export const DEFAULT_INFERENCE_PARAMS: InferenceParams = {
|
||||||
minP: 0.01,
|
minP: 0.01,
|
||||||
repetitionPenalty: 1.0,
|
repetitionPenalty: 1.0,
|
||||||
presencePenalty: 0.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,
|
maxSeqLength: 4096,
|
||||||
maxTokens: 8192,
|
maxTokens: 8192,
|
||||||
systemPrompt: "",
|
systemPrompt: "",
|
||||||
|
|
|
||||||
|
|
@ -40,10 +40,23 @@ const NUMERIC_INFERENCE_FIELDS = [
|
||||||
"minP",
|
"minP",
|
||||||
"repetitionPenalty",
|
"repetitionPenalty",
|
||||||
"presencePenalty",
|
"presencePenalty",
|
||||||
|
"frequencyPenalty",
|
||||||
"maxSeqLength",
|
"maxSeqLength",
|
||||||
"maxTokens",
|
"maxTokens",
|
||||||
] as const satisfies readonly (keyof PersistedInferenceParams)[];
|
] 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>([
|
const CHAT_PRESET_SOURCES = new Set<string>([
|
||||||
"builtin-default",
|
"builtin-default",
|
||||||
"custom",
|
"custom",
|
||||||
|
|
@ -140,6 +153,87 @@ function sanitizeInferenceParams(
|
||||||
if (typeof value.trustRemoteCode === "boolean") {
|
if (typeof value.trustRemoteCode === "boolean") {
|
||||||
params.trustRemoteCode = value.trustRemoteCode;
|
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
|
// Mirror trustRemoteCode handling so the toggle survives reload
|
||||||
// and the /api/chat/settings round-trip.
|
// and the /api/chat/settings round-trip.
|
||||||
if (typeof value.fastMode === "boolean") {
|
if (typeof value.fastMode === "boolean") {
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue