Anthropic Messages API nests `disable_parallel_tool_use` inside the
`tool_choice` object (per docs.claude.com/parallel-tool-use). The local
Anthropic-compat endpoint dropped that flag because the OpenAI shape it
translates into uses a different name and lives at the top level
instead. SDK clients (anthropic-python, anthropic-sdk-go, etc.) that
already speak this dialect therefore could not opt out of parallel
tool calls against the local GGUF model.
Extract `disable_parallel_tool_use` from the incoming tool_choice and
invert it to `parallel_tool_calls` on the agentic-loop call. Plain-chat
and existing tool_choice shapes are untouched. Added a focused unit
test that pins the dict/None/bool/string boundary cases.
Kimi K2.5/K2.6 chat schema documents temperature, top_p and a small
fixed set of knobs; seed and parallel_tool_calls are not in it. The
frontend already hides those controls (provider-capabilities.ts), so
the only way they reach Kimi is a stale client or a direct API caller.
Add them to body_omit so the registry strips them on the wire instead
of relying on the upstream to 400.
Sync the Kimi web-search bypass test to assert both fields are dropped
alongside frequency_penalty/temperature/top_p.
5/10 reviewers in the last round flagged Kimi forwarding non-default frequency_penalty as a 400 risk for K2.5 / K2.6, mirroring the existing lock on temperature and top_p. Hide the slider on the frontend and add frequency_penalty to Kimi's body_omit so even stale clients have the field stripped before the request hits the wire.
service_tier on the generic OpenAI-compatible branch was forwarding whatever value the dispatcher received, so a stale frontend could send standard_only (Anthropic) or scale to providers like Mistral that do not document the field, producing 400s. Gate the forward on an explicit accepts_service_tier=True provider registry opt-in; Anthropic and OpenAI Responses already handle service_tier inside their own helpers.
OpenRouter normalises to OpenAI's chat schema and inherits the 4-entry stop cap. The default 16-cap was too permissive; add stop_max=4 on both the backend provider registry and the frontend PROVIDER_STOP_MAX map.
The GGUF tool-iteration final-answer pass at llama_cpp.py:5182 was carrying only the legacy sampling fields. Forward frequency_penalty, seed, and parallel_tool_calls there too so the cap-exhausted path matches the per-iteration loop.
Test pins the OpenRouter 4-cap.
Kimi documents max 5 stop strings AND <= 32 bytes per string at
https://platform.kimi.ai/docs/api/chat. The previous code capped
count but forwarded oversize entries, which can produce upstream
400s. Add stop_max_bytes=32 on the Kimi registry entry and apply
both checks in a new _normalize_stop_for_provider helper shared
between the default OAI-compat path and the Kimi web-search bypass.
Tests pin the byte-cap drop on both Kimi paths.
Round 5 review flagged two asymmetries:
1. Kimi web-search bypass hard-capped stops at 4 while the default OAI-compat path honours provider_info["stop_max"]. Apply the same provider-aware logic in _stream_kimi_web_search so kimi-with-search and kimi-without-search match. Also add Kimi's documented 5-stop max (https://platform.kimi.ai/docs/api/chat) to the provider registry so the cap actually fires.
2. chat-settings-sheet.tsx caps every non-Anthropic external provider at 4 stops. Replace with a per-provider getProviderStopMax helper in provider-capabilities.ts so DeepSeek, Mistral, and local backends are not artificially restricted while OpenAI Chat still hits its 4-entry hard limit and Kimi hits its documented 5-entry cap.
Tests pin the Kimi 5-cap on both Kimi paths.
Mistral chat completions uses random_seed not seed; map the field via a new seed_field on the provider registry so the new seed control actually works on Mistral. Default for other providers stays seed.
DeepSeek and Mistral both accept up to 16 stop sequences but the default OAI-compat branch was hard-capping at 4 (the OpenAI Chat limit). Studio routes the openai provider through /v1/responses not /v1/chat/completions so the 4-cap only applies if we explicitly added an openai entry. Raise the default to 16 and let per-provider stop_max overrides tighten if needed.
The local GGUF direct chat path (gguf_generate / gguf_generate_with_tools) bypassed _build_openai_passthrough_body and therefore dropped frequency_penalty, seed, stop, and parallel_tool_calls on the floor for users on the default no-tools and with-tools paths. Thread the new fields through LlamaCppBackend.generate_chat_completion and generate_chat_completion_with_tools and the two callsites that invoke them.
Also tighten comments to drop review-process narration that crept in and to remove the em dashes I had introduced in this PR's earlier commits.
Tests pin the Mistral random_seed rename, the DeepSeek 16-cap, and confirm the openai-compat default cap is 16.
Round 4 reviewer consensus (~9/20 independent reviewers) flagged
service_tier=scale as a 400 risk on /v1/responses. The earlier commit
added scale based on the openai-python SDK literal, but the live
OpenAI Responses API reference, the PR's own provider matrix, and the
9-reviewer round-4 consensus all agree the documented Responses enum
is auto|default|flex|priority only. Drop scale on this path to remove
the risk.
Keeps scale on the Chat Completions / OAI-compat path where the SDK
enum is honored and where users who want Scale Tier can still select
it. The widened TypeScript ServiceTier / ServiceTierOption / api.ts
union and the storage sanitizer allowlist remain permissive so legacy
persisted "scale" values do not get silently dropped on reload; the
runtime per-provider gate makes the routing decision.
Tests are updated to pin the restricted Responses enum and the
explicit drop of scale + standard_only + bogus values.
Round 3 reviewer feedback:
- studio/backend/routes/inference.py: _build_chat_request (the
/v1/responses → /v1/chat/completions translator) was dropping
parallel_tool_calls on the floor. A Responses-API caller that set
`parallel_tool_calls=false` saw the flag accepted at the schema
layer but never reach llama-server because the translated
ChatCompletionRequest had no first-class field for it. Now that
parallel_tool_calls IS a first-class field on ChatCompletionRequest
(added by this PR's earlier commits), translate it through the
bridge so the preference actually fires.
- studio/frontend/src/features/chat/utils/chat-settings-storage.ts:
the stop sanitizer silently dropped `stop: []` instead of persisting
the empty array. That meant a user could not clear the last chip —
on reload, the previously-persisted stops came back. Persist empty
arrays explicitly so the cleared state round-trips.
- studio/backend/tests/test_sampling_params_routing.py: pin both with
the raw reproductions reviewers cited.
Round-2 round of review-feedback fixes for the sampling-knobs PR:
- studio/backend/routes/chat_history.py: ChatInferenceSettings still had
the pre-PR field list with extra="forbid", so every settings save the
new frontend issued would 422 on the new keys (frequencyPenalty,
seed, stop, serviceTier, parallelToolCalls). Add the fields with the
same range / enum constraints the chat-completions schema uses, so
the settings-persistence path round-trips cleanly.
- studio/backend/routes/inference.py: _build_passthrough_payload and
_build_openai_passthrough_body now thread frequency_penalty, seed,
and parallel_tool_calls through to llama-server. The frontend exposes
these knobs for local backends; without the forwarding the UI was a
decoration. Each field is gated on `is not None` so 0 / False / "0"
still reach the body.
- studio/backend/core/inference/external_provider.py: the Kimi
$web_search bypass takes an early return into _stream_kimi_web_search
before the default OAI-compat body builder runs, so the new sampling
fields never landed on Kimi-with-search. Forward them through the
helper, with the same dedupe / truncate behavior the main path
applies to `stop`. Also extend the OpenAI Responses service_tier
allowlist to include `scale` per the live openai-python SDK
(response_create_params.py declares
Literal["auto","default","flex","scale","priority"]).
- studio/frontend/src/features/chat/provider-capabilities.ts +
types/runtime.ts: add `scale` to ServiceTier / ServiceTierOption and
surface it on the OpenAI Responses options so the UI matches the
upstream enum.
- studio/backend/tests/test_sampling_params_routing.py: add tests for
every gap above: Kimi web-search bypass forwarding, local OpenAI
passthrough forwarding, ChatSettingsPayload round-trip, and the full
Responses service_tier enum (parametrized over the five accepted
values plus a drop check for the Anthropic-only standard_only).
Anthropic Messages API rejects `disable_parallel_tool_use` as a
top-level field; it is only accepted as a property on the `tool_choice`
object. Move the inversion into a tool_choice merge that defaults to
`{type:"auto"}` when no choice is supplied, and skip the field entirely
when no tools are defined (it is a no-op without tools).
The same path also dropped `stop` chips that contain only whitespace,
because Anthropic 400s with `each stop sequence must contain
non-whitespace` on entries like " ", "\n", and "\n\n". The previous
filter only dropped truly empty strings; switch to `s.strip()` so the
common newline-stop defaults are also filtered out client-side.
Frontend persistence had three round-trip data-loss bugs:
- `VALID_SERVICE_TIERS` was missing `standard_only`, so any Anthropic
user who picked that tier lost it on the next reload.
- The settings sanitizer truncated `stop` to 4 entries on save,
which defeated the Anthropic UI cap of 16. Use 16 here and let the
per-provider stream helper cap to the wire's allowed length.
- The chat-settings sheet's `stopMaxEntries` capped local backends
(llama.cpp / vLLM / ollama / generic OpenAI-compat) at 4 even
though those backends happily accept more. Match Anthropic's 16
for the local path.
Preset policy now carries `frequencyPenalty` and `stop` so a saved
preset can fix a user's preferred decoding style. `seed`,
`serviceTier`, and `parallelToolCalls` stay out of presets because
they are per-request determinism / per-provider account / per-tool
state, not reusable preset values.
Drops the test that pinned the buggy top-level placement of
`disable_parallel_tool_use` and adds two tests for the nested shape
plus the without-tools skip path, plus a test pinning the
whitespace-stop filter against the documented Anthropic error.
- Drop `scale` from the OpenAI service-tier picker (frontend types and
picker option list). OpenAI in Studio routes through `/v1/responses`,
which does not accept `scale`; offering it in the UI silently
dropped the value at the backend and misled users into thinking
their selection was applied. Backend Literal still accepts it on
input so stale clients are not 422'd, and `_stream_openai_responses`
continues to drop it from the wire body.
- Dedupe + drop empty entries for OpenAI Chat `stop` and Anthropic
`stop_sequences` before forwarding so whitespace chips or accidental
repeats do not waste the 4-entry OpenAI cap or the 16-entry
Anthropic cap. Anthropic over-cap now logs and truncates, matching
the OpenAI path.
- Static `aria-label="Parallel tool calls"` on the Switch; screen
readers already announce checked / unchecked state, so the dynamic
Enable/Disable label was redundant.
- Forward an `aria-label` onto the inner Input inside
`StopSequencesInput` so screen-reader users can identify the field.
- Regression tests covering the new dedup, truncation, and the
preserved silent-drop of `scale` on Responses.
Adds the missing sampling parameters that the upstream APIs accept and
that Studio's chat UI previously hid. Each knob is gated per provider
so the picker never offers a field the upstream would 400 on, and the
per-provider stream functions translate / drop fields to match each
API's naming.
New `InferenceParams` fields (round-trip through PersistedInferenceParams
and the chat-settings server store automatically):
- frequencyPenalty (-2..2): OpenAI Chat Completions only.
- seed (int | null): OpenAI Chat + OpenAI-compat local backends.
- stop (string[]): all OpenAI Chat + Anthropic Messages. Backend
truncates to 4 entries on OpenAI Chat per docs and renames to
`stop_sequences` on Anthropic.
- serviceTier (auto|default|flex|priority|scale|standard_only):
per-provider enum sets resolved by getServiceTierOptions.
- parallelToolCalls (bool, default true): forwarded as
`parallel_tool_calls` on both OpenAI APIs and inverted into
`disable_parallel_tool_use` on Anthropic.
OpenAI Responses (gpt-5.x / o3) explicitly drops frequencyPenalty /
seed / stop alongside the existing temperature / top_p drop, since
the upstream 400s on all of them. service_tier on Responses accepts a
subset (no `scale`) which the dispatch already enforces.
UI rows land in the existing Sampling section of the chat settings
sheet using ParamSlider (frequency penalty), a numeric Input (seed),
a new chips editor `StopSequencesInput` (stop), Select (service tier),
and Switch (parallel tool calls). Each row's visibility follows the
new ProviderCapabilities flag.
Tests pin the gating contract: stop_sequences renamed on Anthropic,
4-entry truncation on OpenAI Chat, every Responses-rejected field
dropped, schema-level validation for the service_tier Literal and
frequency_penalty range.
Plan: plans/hashed-riding-porcupine.md