Studio: forward parallel_tool_calls through /v1/responses bridge
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.
This commit is contained in:
parent
d8a4627355
commit
b8cef29b50
3 changed files with 59 additions and 9 deletions
|
|
@ -3780,6 +3780,14 @@ def _build_chat_request(
|
|||
chat_kwargs["top_p"] = payload.top_p
|
||||
if payload.max_output_tokens is not None:
|
||||
chat_kwargs["max_tokens"] = payload.max_output_tokens
|
||||
# `parallel_tool_calls` is now a first-class field on
|
||||
# ChatCompletionRequest (PR #5711) and the OpenAI-compat
|
||||
# passthrough builder forwards it. Translate it here so a Responses
|
||||
# API caller (e.g. OpenAI Codex SDK) that sets
|
||||
# `parallel_tool_calls=false` actually sees the preference reach
|
||||
# llama-server instead of getting silently dropped at the bridge.
|
||||
if payload.parallel_tool_calls is not None:
|
||||
chat_kwargs["parallel_tool_calls"] = payload.parallel_tool_calls
|
||||
|
||||
chat_tools = _translate_responses_tools_to_chat(payload.tools)
|
||||
if chat_tools is not None:
|
||||
|
|
@ -3789,13 +3797,7 @@ def _build_chat_request(
|
|||
if chat_tool_choice is not None:
|
||||
chat_kwargs["tool_choice"] = chat_tool_choice
|
||||
|
||||
req = ChatCompletionRequest(**chat_kwargs)
|
||||
# `parallel_tool_calls` is not a first-class field on ChatCompletionRequest,
|
||||
# but the model allows extras and _build_openai_passthrough_body forwards
|
||||
# only explicitly-known fields. Llama-server does not currently implement
|
||||
# parallel_tool_calls semantics, so we accept-and-ignore it on the
|
||||
# Responses side to avoid breaking SDK clients that always send it.
|
||||
return req
|
||||
return ChatCompletionRequest(**chat_kwargs)
|
||||
|
||||
|
||||
def _chat_tool_calls_to_responses_output(tool_calls: list[dict]) -> list[dict]:
|
||||
|
|
|
|||
|
|
@ -547,6 +547,51 @@ def test_local_openai_passthrough_forwards_new_sampling_fields():
|
|||
assert body["parallel_tool_calls"] is False, body
|
||||
|
||||
|
||||
# ── Responses → ChatCompletions bridge preserves parallel_tool_calls ──
|
||||
|
||||
|
||||
def test_responses_to_chat_bridge_preserves_parallel_tool_calls():
|
||||
"""Round 3 reviewers flagged that `_build_chat_request` (the
|
||||
/v1/responses → /v1/chat/completions translator) dropped
|
||||
`parallel_tool_calls`, so a Responses-API caller that set
|
||||
`parallel_tool_calls=false` never saw the flag reach llama-server.
|
||||
Pin the translation."""
|
||||
from models.inference import ChatMessage, ResponsesRequest
|
||||
from routes.inference import _build_chat_request, _build_openai_passthrough_body
|
||||
|
||||
payload = ResponsesRequest(
|
||||
input = "hi",
|
||||
stream = True,
|
||||
parallel_tool_calls = False,
|
||||
)
|
||||
chat_req = _build_chat_request(
|
||||
payload,
|
||||
[ChatMessage(role = "user", content = "hi")],
|
||||
stream = True,
|
||||
)
|
||||
assert chat_req.parallel_tool_calls is False, chat_req
|
||||
body = _build_openai_passthrough_body(chat_req, backend_ctx = 4096)
|
||||
assert body["parallel_tool_calls"] is False, body
|
||||
|
||||
|
||||
def test_responses_to_chat_bridge_omits_unset_parallel_tool_calls():
|
||||
"""Unset `parallel_tool_calls` (None) must not appear on the
|
||||
translated body — the upstream default is `true` everywhere, so
|
||||
forwarding `parallel_tool_calls=None` would over-specify."""
|
||||
from models.inference import ChatMessage, ResponsesRequest
|
||||
from routes.inference import _build_chat_request, _build_openai_passthrough_body
|
||||
|
||||
payload = ResponsesRequest(input = "hi", stream = True)
|
||||
chat_req = _build_chat_request(
|
||||
payload,
|
||||
[ChatMessage(role = "user", content = "hi")],
|
||||
stream = True,
|
||||
)
|
||||
assert chat_req.parallel_tool_calls is None, chat_req
|
||||
body = _build_openai_passthrough_body(chat_req, backend_ctx = 4096)
|
||||
assert "parallel_tool_calls" not in body, body
|
||||
|
||||
|
||||
# ── Backend ChatInferenceSettings schema accepts new fields ────────────
|
||||
|
||||
|
||||
|
|
|
|||
|
|
@ -171,10 +171,13 @@ function sanitizeInferenceParams(
|
|||
// re-truncates to the wire cap (4 for OpenAI Chat, 16 for Anthropic,
|
||||
// dropped entirely for OpenAI Responses) before the request hits the
|
||||
// network. Capping to 4 here would defeat Anthropic's UI cap of 16
|
||||
// for users who switch providers between sessions.
|
||||
// for users who switch providers between sessions. An EMPTY array
|
||||
// must persist as an empty array (not be sanitized away) so the user
|
||||
// can clear the last chip and have the change saved — otherwise the
|
||||
// previously stored stops come back on reload.
|
||||
if (Array.isArray(value.stop)) {
|
||||
const stops = value.stop.filter((s): s is string => typeof s === "string");
|
||||
if (stops.length > 0) params.stop = stops.slice(0, 16);
|
||||
params.stop = stops.slice(0, 16);
|
||||
}
|
||||
// serviceTier: nullable enum string.
|
||||
if (value.serviceTier === null) {
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue