diff --git a/studio/backend/core/inference/external_provider.py b/studio/backend/core/inference/external_provider.py index 002e2548c6..d55f132487 100644 --- a/studio/backend/core/inference/external_provider.py +++ b/studio/backend/core/inference/external_provider.py @@ -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) diff --git a/studio/frontend/src/components/ui/stop-sequences-input.tsx b/studio/frontend/src/components/ui/stop-sequences-input.tsx index e3c2d8a561..5d65fdd36a 100644 --- a/studio/frontend/src/components/ui/stop-sequences-input.tsx +++ b/studio/frontend/src/components/ui/stop-sequences-input.tsx @@ -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(""); } diff --git a/studio/frontend/src/features/chat/types/api.ts b/studio/frontend/src/features/chat/types/api.ts index e94f9dca7c..7c5b5bf99f 100644 --- a/studio/frontend/src/features/chat/types/api.ts +++ b/studio/frontend/src/features/chat/types/api.ts @@ -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.