studio: drop temperature/top_p for OpenAI reasoning models

gpt-5.x / o3 / gpt-4.5 are reasoning-class models served via
/v1/responses, and reject temperature and top_p with
'Unsupported parameter' 400s. The OpenAI registry allowlist already
scopes the picker to those families, so neither knob ever applies on
this branch.

- external_provider._stream_openai_responses no longer puts
  temperature or top_p in the request body (kept on the method
  signature for API symmetry with the other stream methods).
- ProviderCapabilities gains temperature/topP flags; OpenAI sets both
  to false. ChatSettingsPanel hides the sliders for OpenAI so the user
  does not see inert controls.
- chat-adapter omits temperature/top_p from the external request body
  when the active provider does not advertise them.
- OpenAIChatCompletionsRequest type marks both as optional, matching
  the new chat-adapter shape.
- test_responses_request_body_uses_input_and_instructions: assertions
  flipped to confirm temperature / top_p are absent from the body.
This commit is contained in:
Roland Tannous 2026-05-12 13:58:37 +04:00
commit eeed153e2f
6 changed files with 73 additions and 32 deletions

View file

@ -469,12 +469,20 @@ class ExternalProviderClient:
if translated_parts:
input_items.append({"role": role, "content": translated_parts})
# NOTE: gpt-5.x / o3 / gpt-4.5 are reasoning-class models. They reject
# temperature and top_p with `Unsupported parameter` 400s on
# /v1/responses (and on /v1/chat/completions for the same families).
# The PROVIDER_REGISTRY['openai'] model_id_allowlist already scopes
# the picker to those families, so we never need to send sampling
# knobs here. ``reasoning.effort`` defaults to "medium" server-side
# if omitted — surface it in a future commit if a knob is wanted.
del temperature, top_p # explicit drop — params are accepted for
# API symmetry with the other stream methods but not forwarded.
body: dict[str, Any] = {
"model": model,
"input": input_items,
"stream": True,
"temperature": temperature,
"top_p": top_p,
}
if instructions_parts:
body["instructions"] = "\n\n".join(instructions_parts)

View file

@ -97,12 +97,13 @@ def test_responses_request_body_uses_input_and_instructions(monkeypatch):
assert body["model"] == "gpt-5.5"
assert body["instructions"] == "You are concise."
assert body["input"] == [{"role": "user", "content": "Hi"}]
assert body["temperature"] == 0.5
assert body["top_p"] == 0.9
assert body["max_output_tokens"] == 512
assert body["stream"] is True
# Responses API does not accept these — frontend caps + backend path both
# strip them; make sure we never silently forward them.
# Responses API on reasoning-class models (gpt-5.x / o3 / gpt-4.5 — the
# only OpenAI ids the registry allowlist exposes) rejects these as
# `Unsupported parameter`. Make sure we never silently forward them.
assert "temperature" not in body
assert "top_p" not in body
assert "presence_penalty" not in body
assert "frequency_penalty" not in body
assert "top_k" not in body

View file

@ -860,8 +860,14 @@ export function createOpenAIStreamAdapter(): ChatModelAdapter {
model: externalSelection.modelId,
messages: outboundMessages,
stream: true,
temperature: params.temperature,
top_p: params.topP,
// Reasoning-class models (OpenAI gpt-5.x / o3) reject temperature
// and top_p; only forward when the active provider supports them.
...(externalCapabilities?.temperature !== false
? { temperature: params.temperature }
: {}),
...(externalCapabilities?.topP !== false
? { top_p: params.topP }
: {}),
// Clamp to the cross-provider output cap so a maxTokens value
// carried over from a local-model session does not blow past
// provider limits (e.g. Claude Opus 400s on >128k).

View file

@ -532,6 +532,9 @@ export function ChatSettingsPanel({
// is only consulted when `isExternalModel` is true. An external model with an
// unknown provider falls back to the OpenAI-compat shape via
// getProviderCapabilities, so these flags never undercount support.
const showTemperature =
!isExternalModel || Boolean(providerCapabilities?.temperature);
const showTopP = !isExternalModel || Boolean(providerCapabilities?.topP);
const showTopK = !isExternalModel || Boolean(providerCapabilities?.topK);
const showMinP = !isExternalModel || Boolean(providerCapabilities?.minP);
const showRepetitionPenalty =
@ -1155,25 +1158,29 @@ export function ChatSettingsPanel({
<CollapsibleSection label="Sampling" defaultOpen={true}>
<div className="flex flex-col gap-5 pt-1">
<ParamSlider
label="Temperature"
value={params.temperature}
min={0}
max={2}
step={0.01}
onChange={set("temperature")}
info="Controls randomness. Lower values make output focused and deterministic; higher values increase variety and creativity."
/>
<ParamSlider
label="Top P"
value={params.topP}
min={0}
max={1}
step={0.05}
onChange={set("topP")}
displayValue={params.topP === 1 ? "Off" : undefined}
info="Nucleus sampling. Restricts choices to the smallest set of tokens whose cumulative probability reaches this threshold. 1.0 = off."
/>
{showTemperature ? (
<ParamSlider
label="Temperature"
value={params.temperature}
min={0}
max={2}
step={0.01}
onChange={set("temperature")}
info="Controls randomness. Lower values make output focused and deterministic; higher values increase variety and creativity."
/>
) : null}
{showTopP ? (
<ParamSlider
label="Top P"
value={params.topP}
min={0}
max={1}
step={0.05}
onChange={set("topP")}
displayValue={params.topP === 1 ? "Off" : undefined}
info="Nucleus sampling. Restricts choices to the smallest set of tokens whose cumulative probability reaches this threshold. 1.0 = off."
/>
) : null}
{showTopK ? (
<ParamSlider
label="Top K"

View file

@ -14,6 +14,13 @@
*/
export interface ProviderCapabilities {
/**
* Temperature sampling. Reasoning-class models (OpenAI's gpt-5.x / o3 via
* /v1/responses) reject this with `Unsupported parameter`.
*/
temperature: boolean;
/** Nucleus (top_p) sampling. Same restriction as `temperature` on OpenAI. */
topP: boolean;
/** top-k token sampling (only Anthropic on the providers we ship). */
topK: boolean;
/** min-p token cutoff (no SaaS provider currently exposes this). */
@ -38,6 +45,8 @@ export interface ProviderCapabilities {
export const EXTERNAL_MAX_OUTPUT_TOKENS = 32768;
const OPENAI_COMPAT_BASE: ProviderCapabilities = {
temperature: true,
topP: true,
topK: false,
minP: false,
repetitionPenalty: false,
@ -45,6 +54,8 @@ const OPENAI_COMPAT_BASE: ProviderCapabilities = {
};
const ALL_SUPPORTED: ProviderCapabilities = {
temperature: true,
topP: true,
topK: true,
minP: true,
repetitionPenalty: true,
@ -52,10 +63,13 @@ const ALL_SUPPORTED: ProviderCapabilities = {
};
const PROVIDER_CAPABILITIES: Record<string, ProviderCapabilities> = {
// OpenAI's flagship models (gpt-5.x) now require /v1/responses, and the
// Responses API drops presence/frequency penalty from the contract — see
// backend external_provider._stream_openai_responses for the proxy.
// OpenAI's flagship models (gpt-5.x / o3 / gpt-4.5) are reasoning-class
// models served via /v1/responses, which rejects temperature, top_p, and
// presence/frequency penalty. See backend
// external_provider._stream_openai_responses for the proxy.
openai: {
temperature: false,
topP: false,
topK: false,
minP: false,
repetitionPenalty: false,
@ -63,6 +77,8 @@ const PROVIDER_CAPABILITIES: Record<string, ProviderCapabilities> = {
},
// Anthropic's Messages API accepts top_k but not presence/frequency penalty.
anthropic: {
temperature: true,
topP: true,
topK: true,
minP: false,
repetitionPenalty: false,
@ -72,6 +88,8 @@ const PROVIDER_CAPABILITIES: Record<string, ProviderCapabilities> = {
gemini: OPENAI_COMPAT_BASE,
// DeepSeek deprecated presence/frequency penalty in their current docs.
deepseek: {
temperature: true,
topP: true,
topK: false,
minP: false,
repetitionPenalty: false,

View file

@ -190,8 +190,9 @@ export interface OpenAIChatCompletionsRequest {
model: string;
messages: OpenAIChatMessage[];
stream: boolean;
temperature: number;
top_p: number;
/** Reasoning-class OpenAI models reject these — caller may omit. */
temperature?: number;
top_p?: number;
max_tokens: number;
top_k?: number;
min_p?: number;