Studio: widen scale type, preserve significant ws in stops, Kimi parity

Round 2 reviewer feedback:

- studio/frontend/src/features/chat/types/api.ts: `OpenAIChatCompletionsRequest.service_tier` did not include `"scale"`, so the request builder in chat-adapter.ts failed typecheck after the runtime ServiceTier union widened (`Type 'ServiceTier | undefined' is not assignable...`). Widen the type to match the SDK and keep the typecheck green.

- studio/frontend/src/components/ui/stop-sequences-input.tsx: the chip editor used `draft.trim()` for storage, which silently mutated semantically meaningful stops like " End", "### ", and "\n\n". Keep the whitespace-only rejection (Anthropic 400s on those, OpenAI silently drops them) but persist the raw draft so leading/trailing whitespace inside otherwise-meaningful stops survives.

- studio/backend/core/inference/external_provider.py: the Kimi web-search bypass dropped a single string `stop="\n\n"` via `stop.strip()` while the normal default OAI-compat path forwards it verbatim. Mirror the default path's behavior here so kimi-with-search and kimi-without-search apply the same rules (asymmetric provider-path fix flagged in round-2 review).
This commit is contained in:
Daniel Han 2026-05-24 13:44:38 +00:00
commit d8a4627355
3 changed files with 28 additions and 8 deletions

View file

@ -912,9 +912,15 @@ class ExternalProviderClient:
if seed is not None:
body["seed"] = seed
if stop:
# Mirror the default OAI-compat path's stop handling exactly
# so Kimi-with-search and Kimi-without-search apply the
# same rules — a single string is forwarded verbatim and
# lists are deduped + truncated to OpenAI's 4-entry cap.
# Earlier the bypass dropped whitespace-only strings here
# while the normal path forwarded them, which was an
# asymmetric provider-path fix.
if isinstance(stop, str):
if stop.strip():
body["stop"] = stop
body["stop"] = stop
elif isinstance(stop, list):
sequences = list(
dict.fromkeys(s for s in stop if isinstance(s, str) and s)

View file

@ -38,14 +38,20 @@ export function StopSequencesInput({
const atCap = value.length >= maxEntries;
function commitDraft() {
const trimmed = draft.trim();
if (!trimmed) return;
// Reject chips that are empty or contain ONLY whitespace
// (Anthropic 400s on those and OpenAI silently drops them), but
// preserve significant leading/trailing whitespace inside otherwise
// -meaningful stops like " END", "### ", or "\n\n" — stop matching
// is exact, so stripping would silently change the semantics. The
// backend re-validates per-provider before the request hits the
// wire.
if (!draft || !draft.trim()) return;
if (atCap) return;
if (value.includes(trimmed)) {
if (value.includes(draft)) {
setDraft("");
return;
}
onChange([...value, trimmed]);
onChange([...value, draft]);
setDraft("");
}

View file

@ -280,14 +280,22 @@ export interface OpenAIChatCompletionsRequest {
stop?: string[];
/**
* Provider service tier. Anthropic accepts `auto|standard_only`;
* OpenAI Chat accepts `auto|default|flex|priority|scale`; OpenAI
* Responses accepts `auto|default|flex|priority`.
* OpenAI Chat + Responses both accept
* `auto|default|flex|scale|priority` per the live `openai-python`
* SDK (`src/openai/types/responses/response_create_params.py`
* declares `Optional[Literal["auto", "default", "flex", "scale",
* "priority"]]`). The wire-side helper in
* `studio/backend/core/inference/external_provider.py` drops values
* that a given provider does not accept; this union stays permissive
* so the request-builder typechecks against
* `InferenceParams.serviceTier` without per-provider narrowing.
*/
service_tier?:
| "auto"
| "default"
| "flex"
| "priority"
| "scale"
| "standard_only";
/**
* Whether the provider may dispatch tool calls in parallel.