Per-model OpenAI / Anthropic 4.7 sampling gating (PR #5711)
OpenAI gating was per-provider — the restrictive reasoning-class capability
applied to gpt-4o too, even though gpt-4o on /v1/responses still accepts
temperature / top_p / seed / frequency_penalty / presence_penalty. Anthropic
4.7 was the inverse: backend stripped temperature / top_p / top_k per-model
but the UI still showed the sliders, so moving a knob silently did nothing.
Split openai capabilities into OPENAI_REASONING_CAPABILITIES (current
restrictive set, used for gpt-5.x / o1 / o3 / o4) and OPENAI_CHAT_CAPABILITIES
(full sampling minus top_k and stop, used for gpt-4o and any non-reasoning id
from the registry). Mirror the backend _ANTHROPIC_4_7_SAMPLING_REMOVED regex
on the frontend so claude-(opus|sonnet|haiku)-4-7 hides temperature / top_p /
top_k in the panel instead of relying on backend strip. getProviderCapabilities
now takes an optional modelId so chat-settings-sheet and chat-adapter both
resolve the same per-model variant; no behavior change for unspecified modelId.
Verified live OpenAI / Anthropic docs:
- GPT-5 temperature must equal 1: platform.openai.com/docs/guides/reasoning,
community.openai.com/t/temperature-in-gpt-5-models/1337133
- GPT-4o accepts full sampling on Responses: docs.aimlapi.com gpt-4o ref,
OpenAI cookbook seed example
- Claude 4.7 sampling removed (400 on any non-default temperature/top_p/
top_k): platform.claude.com/docs/en/about-claude/models/whats-new-claude-4-7
160 backend routing tests still pass; frontend tsc clean.
This commit is contained in:
parent
5d4ddd6b37
commit
643c0a88f1
3 changed files with 106 additions and 24 deletions
|
|
@ -1190,6 +1190,7 @@ export function createOpenAIStreamAdapter(): ChatModelAdapter {
|
|||
);
|
||||
const externalCapabilities = getProviderCapabilities(
|
||||
externalProvider?.providerType,
|
||||
externalSelection?.modelId,
|
||||
);
|
||||
const externalReasoningCaps: ReturnType<
|
||||
typeof getExternalReasoningCapabilities
|
||||
|
|
|
|||
|
|
@ -696,7 +696,10 @@ export function ChatPage(): ReactElement {
|
|||
const provider = externalProvidersForChat.find(
|
||||
(p) => p.id === selection.providerId,
|
||||
);
|
||||
const baseCapabilities = getProviderCapabilities(provider?.providerType);
|
||||
const baseCapabilities = getProviderCapabilities(
|
||||
provider?.providerType,
|
||||
selection.modelId,
|
||||
);
|
||||
if (!baseCapabilities) return baseCapabilities;
|
||||
const anthropicThinkingEnabled =
|
||||
provider?.providerType === "anthropic" &&
|
||||
|
|
|
|||
|
|
@ -395,26 +395,87 @@ const ALL_SUPPORTED: ProviderCapabilities = {
|
|||
parallelToolCalls: true,
|
||||
};
|
||||
|
||||
// Reasoning-class OpenAI models served via /v1/responses fix temperature
|
||||
// at 1, ignore top_p, and 400 on presence/frequency_penalty / seed. Non
|
||||
// reasoning models (gpt-4o, gpt-4-turbo, gpt-4, gpt-3.5-turbo) keep the
|
||||
// full sampling surface even when routed through /v1/responses. See
|
||||
// https://platform.openai.com/docs/guides/reasoning and the GPT-5 release
|
||||
// notes; backend dispatch is external_provider._stream_openai_responses.
|
||||
// The Responses API itself drops `stop`, so we leave that off for all
|
||||
// OpenAI models regardless of family.
|
||||
const OPENAI_REASONING_CAPABILITIES: ProviderCapabilities = {
|
||||
temperature: false,
|
||||
topP: false,
|
||||
topK: false,
|
||||
minP: false,
|
||||
repetitionPenalty: false,
|
||||
presencePenalty: false,
|
||||
frequencyPenalty: false,
|
||||
seed: false,
|
||||
stop: false,
|
||||
serviceTier: true,
|
||||
parallelToolCalls: true,
|
||||
};
|
||||
const OPENAI_CHAT_CAPABILITIES: ProviderCapabilities = {
|
||||
temperature: true,
|
||||
topP: true,
|
||||
topK: false,
|
||||
minP: false,
|
||||
repetitionPenalty: false,
|
||||
presencePenalty: true,
|
||||
frequencyPenalty: true,
|
||||
seed: true,
|
||||
// Responses API does not surface `stop` even for non-reasoning models;
|
||||
// tracked in OpenAI's Responses-vs-ChatCompletions migration notes.
|
||||
stop: false,
|
||||
serviceTier: true,
|
||||
parallelToolCalls: true,
|
||||
};
|
||||
|
||||
// Prefix list for OpenAI reasoning-class model ids. Kept in sync with
|
||||
// OPENAI_REASONING_MODELS below (used for reasoning_effort capability).
|
||||
// Longest prefixes first so "gpt-5.5-pro" wins over "gpt-5.5".
|
||||
const OPENAI_REASONING_MODEL_PREFIXES = [
|
||||
"gpt-5.5-pro",
|
||||
"gpt-5.5",
|
||||
"gpt-5.4-pro",
|
||||
"gpt-5.4",
|
||||
"gpt-5.3-chat-latest",
|
||||
"gpt-5.3-codex",
|
||||
"gpt-5.3",
|
||||
"gpt-5.2",
|
||||
"gpt-5.1",
|
||||
"gpt-5",
|
||||
"o1",
|
||||
"o3",
|
||||
"o4",
|
||||
] as const;
|
||||
|
||||
function isOpenAIReasoningModelId(modelId: string | null | undefined): boolean {
|
||||
const normalized = modelId?.trim().toLowerCase() ?? "";
|
||||
if (!normalized) return false;
|
||||
return OPENAI_REASONING_MODEL_PREFIXES.some((p) => normalized.startsWith(p));
|
||||
}
|
||||
|
||||
// Mirror of backend _ANTHROPIC_4_7_SAMPLING_REMOVED in
|
||||
// studio/backend/core/inference/external_provider.py:110. Claude 4.7
|
||||
// (Opus/Sonnet/Haiku) removed temperature, top_p, and top_k entirely;
|
||||
// surfacing the sliders would let the user move a control that the
|
||||
// backend silently strips. The trailing -4-7[-.]/EOL anchor keeps future
|
||||
// families (claude-opus-5 etc.) unaffected.
|
||||
const ANTHROPIC_4_7_SAMPLING_REMOVED_REGEX = /^claude-(?:opus|sonnet|haiku)-4-7(?:[-.]|$)/i;
|
||||
|
||||
function isClaude47SamplingRemoved(modelId: string | null | undefined): boolean {
|
||||
const normalized = modelId?.trim().toLowerCase() ?? "";
|
||||
if (!normalized) return false;
|
||||
return ANTHROPIC_4_7_SAMPLING_REMOVED_REGEX.test(normalized);
|
||||
}
|
||||
|
||||
const PROVIDER_CAPABILITIES: Record<string, ProviderCapabilities> = {
|
||||
// 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.
|
||||
// service_tier and parallel_tool_calls are accepted on /v1/responses;
|
||||
// seed / stop / frequency_penalty are 400'd alongside temperature/top_p.
|
||||
openai: {
|
||||
temperature: false,
|
||||
topP: false,
|
||||
topK: false,
|
||||
minP: false,
|
||||
repetitionPenalty: false,
|
||||
presencePenalty: false,
|
||||
frequencyPenalty: false,
|
||||
seed: false,
|
||||
stop: false,
|
||||
serviceTier: true,
|
||||
parallelToolCalls: true,
|
||||
},
|
||||
// Default OpenAI bucket is reasoning-class (current registry only ships
|
||||
// gpt-5.x / o3 ids), but per-model resolution in getProviderCapabilities
|
||||
// upgrades non-reasoning ids (gpt-4o etc.) to OPENAI_CHAT_CAPABILITIES.
|
||||
openai: OPENAI_REASONING_CAPABILITIES,
|
||||
// Anthropic's Messages API accepts top_k on 3.x and 4.5/4.6, but Claude
|
||||
// 4.7 (Opus/Sonnet/Haiku) deprecated it and returns 400 if it is set.
|
||||
// We surface top_k in the panel for all Anthropic providers and let the
|
||||
|
|
@ -496,15 +557,32 @@ const PROVIDER_CAPABILITIES: Record<string, ProviderCapabilities> = {
|
|||
const DEFAULT_EXTERNAL_CAPABILITIES = OPENAI_COMPAT_BASE;
|
||||
|
||||
/**
|
||||
* Resolve the capability set for an external provider. Returns `null` for
|
||||
* a local model (i.e. when `providerType` is null/undefined), which callers
|
||||
* should treat as "every knob applies".
|
||||
* Resolve the capability set for an external provider, optionally
|
||||
* specialised by model id. Returns `null` for a local model (i.e. when
|
||||
* `providerType` is null/undefined), which callers should treat as
|
||||
* "every knob applies".
|
||||
*
|
||||
* Per-model specialisations:
|
||||
* - openai + non-reasoning model (gpt-4o, gpt-4-turbo, gpt-4,
|
||||
* gpt-3.5-turbo): full sampling surface (OPENAI_CHAT_CAPABILITIES).
|
||||
* - openai + reasoning model (gpt-5.x, o1, o3, o4): restrictive
|
||||
* (OPENAI_REASONING_CAPABILITIES).
|
||||
* - anthropic + claude-*-4-7: temperature/top_p/top_k stripped to
|
||||
* match the backend 400-avoidance regex.
|
||||
*/
|
||||
export function getProviderCapabilities(
|
||||
providerType: string | null | undefined,
|
||||
modelId?: string | null | undefined,
|
||||
): ProviderCapabilities | null {
|
||||
if (!providerType) return null;
|
||||
return PROVIDER_CAPABILITIES[providerType] ?? DEFAULT_EXTERNAL_CAPABILITIES;
|
||||
const base = PROVIDER_CAPABILITIES[providerType] ?? DEFAULT_EXTERNAL_CAPABILITIES;
|
||||
if (providerType === "openai" && modelId && !isOpenAIReasoningModelId(modelId)) {
|
||||
return OPENAI_CHAT_CAPABILITIES;
|
||||
}
|
||||
if (providerType === "anthropic" && isClaude47SamplingRemoved(modelId)) {
|
||||
return { ...base, temperature: false, topP: false, topK: false };
|
||||
}
|
||||
return base;
|
||||
}
|
||||
|
||||
const DEFAULT_EFFORT_LEVELS = ["low", "medium", "high"] as const;
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue