studio/chat: drive ChatSettingsPanel from a per-provider capability map
Replace the binary isExternalModel toggle in the sampling section with a provider-aware capability map. Each external provider type advertises which of top_k / min_p / repetition_penalty / presence_penalty its chat-completions API actually accepts, so the panel only renders the knobs that map onto the active provider's request body. Anthropic now exposes top_k; DeepSeek hides presence_penalty (deprecated in their docs); OpenRouter and custom providers continue to show every knob (OpenRouter drops unsupported server-side, custom assumes OpenAI-compat or a permissive vLLM/Ollama backend). Local models are unaffected — null capabilities means 'show everything'. chat-adapter.ts now forwards top_k / presence_penalty to the external proxy only when the active provider's capabilities permit it, so the request body matches what the UI shows.
This commit is contained in:
parent
9fb94feeac
commit
f40b5fbe75
4 changed files with 176 additions and 46 deletions
|
|
@ -26,6 +26,7 @@ import {
|
|||
loadExternalProviders,
|
||||
parseExternalModelId,
|
||||
} from "../external-providers";
|
||||
import { getProviderCapabilities } from "../provider-capabilities";
|
||||
import { useChatRuntimeStore } from "../stores/chat-runtime-store";
|
||||
import { isMultimodalResponse } from "../types/api";
|
||||
import type { ChatModelSummary } from "../types/runtime";
|
||||
|
|
@ -847,6 +848,9 @@ export function createOpenAIStreamAdapter(): ChatModelAdapter {
|
|||
externalProvider?.providerType === "custom"
|
||||
? "openai"
|
||||
: externalProvider?.providerType;
|
||||
const externalCapabilities = getProviderCapabilities(
|
||||
externalProvider?.providerType,
|
||||
);
|
||||
const buildRequestPayload = async (forceRefreshPublicKey = false) => {
|
||||
if (externalSelection && externalProvider) {
|
||||
return {
|
||||
|
|
@ -856,7 +860,14 @@ export function createOpenAIStreamAdapter(): ChatModelAdapter {
|
|||
temperature: params.temperature,
|
||||
top_p: params.topP,
|
||||
max_tokens: params.maxTokens,
|
||||
presence_penalty: params.presencePenalty,
|
||||
// Only forward sampling knobs the provider actually accepts; the
|
||||
// backend's external-provider proxy is param-permissive and would
|
||||
// surface a 400 from providers that reject unknown fields (e.g.
|
||||
// OpenAI rejects top_k, Anthropic/DeepSeek reject presence_penalty).
|
||||
...(externalCapabilities?.topK ? { top_k: params.topK } : {}),
|
||||
...(externalCapabilities?.presencePenalty
|
||||
? { presence_penalty: params.presencePenalty }
|
||||
: {}),
|
||||
provider_id: externalProvider.id,
|
||||
provider_type: externalBackendProviderType,
|
||||
external_model: externalSelection.modelId,
|
||||
|
|
|
|||
|
|
@ -42,7 +42,12 @@ import { ChatSettingsPanel } from "./chat-settings-sheet";
|
|||
import { ContextUsageBar } from "./components/context-usage-bar";
|
||||
import { ModelLoadInlineStatus } from "./components/model-load-status";
|
||||
import { db } from "./db";
|
||||
import { buildExternalModelId, isExternalModelId } from "./external-providers";
|
||||
import {
|
||||
buildExternalModelId,
|
||||
isExternalModelId,
|
||||
parseExternalModelId,
|
||||
} from "./external-providers";
|
||||
import { getProviderCapabilities } from "./provider-capabilities";
|
||||
import { useChatModelRuntime } from "./hooks/use-chat-model-runtime";
|
||||
import {
|
||||
clearTrainingCompareHandoff,
|
||||
|
|
@ -616,6 +621,14 @@ export function ChatPage(): ReactElement {
|
|||
() => isExternalModelId(inferenceParams.checkpoint),
|
||||
[inferenceParams.checkpoint],
|
||||
);
|
||||
const activeProviderCapabilities = useMemo(() => {
|
||||
const selection = parseExternalModelId(inferenceParams.checkpoint);
|
||||
if (!selection) return null;
|
||||
const provider = externalProviders.find(
|
||||
(p) => p.id === selection.providerId,
|
||||
);
|
||||
return getProviderCapabilities(provider?.providerType);
|
||||
}, [externalProviders, inferenceParams.checkpoint]);
|
||||
const canCompare = useMemo(() => {
|
||||
return Boolean(inferenceParams.checkpoint) && !isExternalModel;
|
||||
}, [inferenceParams.checkpoint, isExternalModel]);
|
||||
|
|
@ -1188,6 +1201,7 @@ export function ChatPage(): ReactElement {
|
|||
params={inferenceParams}
|
||||
onParamsChange={setInferenceParams}
|
||||
isExternalModel={isExternalModel}
|
||||
providerCapabilities={activeProviderCapabilities}
|
||||
onReloadModel={() => {
|
||||
const state = useChatRuntimeStore.getState();
|
||||
if (state.params.checkpoint) {
|
||||
|
|
|
|||
|
|
@ -80,6 +80,7 @@ import {
|
|||
toPresetParams,
|
||||
type Preset,
|
||||
} from "./presets/preset-policy";
|
||||
import type { ProviderCapabilities } from "./provider-capabilities";
|
||||
import type { InferenceParams } from "./types/runtime";
|
||||
|
||||
export { defaultInferenceParams, type Preset } from "./presets/preset-policy";
|
||||
|
|
@ -506,6 +507,12 @@ interface ChatSettingsPanelProps {
|
|||
params: InferenceParams;
|
||||
onParamsChange: (params: InferenceParams) => void;
|
||||
isExternalModel?: boolean;
|
||||
/**
|
||||
* Sampling-param capability set for the active external provider, or `null`
|
||||
* for local models (in which case every knob is rendered). Drives the
|
||||
* per-param visibility in the sampling section.
|
||||
*/
|
||||
providerCapabilities?: ProviderCapabilities | null;
|
||||
onReloadModel?: () => void;
|
||||
}
|
||||
|
||||
|
|
@ -515,8 +522,19 @@ export function ChatSettingsPanel({
|
|||
params,
|
||||
onParamsChange,
|
||||
isExternalModel = false,
|
||||
providerCapabilities = null,
|
||||
onReloadModel,
|
||||
}: ChatSettingsPanelProps) {
|
||||
// For non-external (local) models we show every knob — providerCapabilities
|
||||
// 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 showTopK = !isExternalModel || Boolean(providerCapabilities?.topK);
|
||||
const showMinP = !isExternalModel || Boolean(providerCapabilities?.minP);
|
||||
const showRepetitionPenalty =
|
||||
!isExternalModel || Boolean(providerCapabilities?.repetitionPenalty);
|
||||
const showPresencePenalty =
|
||||
!isExternalModel || Boolean(providerCapabilities?.presencePenalty);
|
||||
const isMobile = useIsMobile();
|
||||
const isGguf = useChatRuntimeStore((s) => s.activeGgufVariant) != null;
|
||||
const hasModelContent =
|
||||
|
|
@ -1153,51 +1171,55 @@ export function ChatSettingsPanel({
|
|||
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."
|
||||
/>
|
||||
{!isExternalModel ? (
|
||||
<>
|
||||
<ParamSlider
|
||||
label="Top K"
|
||||
value={params.topK}
|
||||
min={0}
|
||||
max={100}
|
||||
step={1}
|
||||
onChange={set("topK")}
|
||||
displayValue={params.topK === 0 ? "Off" : undefined}
|
||||
info="Limits sampling to the K most likely tokens at each step. 0 = off."
|
||||
/>
|
||||
<ParamSlider
|
||||
label="Min P"
|
||||
value={params.minP}
|
||||
min={0}
|
||||
max={1}
|
||||
step={0.01}
|
||||
onChange={set("minP")}
|
||||
info="Drops tokens whose probability is below this fraction of the top token's probability. Filters unlikely candidates."
|
||||
/>
|
||||
<ParamSlider
|
||||
label="Repetition Penalty"
|
||||
value={params.repetitionPenalty}
|
||||
min={1}
|
||||
max={2}
|
||||
step={0.05}
|
||||
onChange={set("repetitionPenalty")}
|
||||
displayValue={
|
||||
params.repetitionPenalty === 1 ? "Off" : undefined
|
||||
}
|
||||
info="Down-weights tokens that have already appeared, reducing repetition. 1.0 = off; higher values penalize more strongly."
|
||||
/>
|
||||
</>
|
||||
{showTopK ? (
|
||||
<ParamSlider
|
||||
label="Top K"
|
||||
value={params.topK}
|
||||
min={0}
|
||||
max={100}
|
||||
step={1}
|
||||
onChange={set("topK")}
|
||||
displayValue={params.topK === 0 ? "Off" : undefined}
|
||||
info="Limits sampling to the K most likely tokens at each step. 0 = off."
|
||||
/>
|
||||
) : null}
|
||||
{showMinP ? (
|
||||
<ParamSlider
|
||||
label="Min P"
|
||||
value={params.minP}
|
||||
min={0}
|
||||
max={1}
|
||||
step={0.01}
|
||||
onChange={set("minP")}
|
||||
info="Drops tokens whose probability is below this fraction of the top token's probability. Filters unlikely candidates."
|
||||
/>
|
||||
) : null}
|
||||
{showRepetitionPenalty ? (
|
||||
<ParamSlider
|
||||
label="Repetition Penalty"
|
||||
value={params.repetitionPenalty}
|
||||
min={1}
|
||||
max={2}
|
||||
step={0.05}
|
||||
onChange={set("repetitionPenalty")}
|
||||
displayValue={
|
||||
params.repetitionPenalty === 1 ? "Off" : undefined
|
||||
}
|
||||
info="Down-weights tokens that have already appeared, reducing repetition. 1.0 = off; higher values penalize more strongly."
|
||||
/>
|
||||
) : null}
|
||||
{showPresencePenalty ? (
|
||||
<ParamSlider
|
||||
label="Presence Penalty"
|
||||
value={params.presencePenalty}
|
||||
min={0}
|
||||
max={2}
|
||||
step={0.1}
|
||||
onChange={set("presencePenalty")}
|
||||
displayValue={params.presencePenalty === 0 ? "Off" : undefined}
|
||||
info="Penalizes any token that has already appeared at least once, encouraging the model to introduce new topics. 0 = off."
|
||||
/>
|
||||
) : null}
|
||||
<ParamSlider
|
||||
label="Presence Penalty"
|
||||
value={params.presencePenalty}
|
||||
min={0}
|
||||
max={2}
|
||||
step={0.1}
|
||||
onChange={set("presencePenalty")}
|
||||
displayValue={params.presencePenalty === 0 ? "Off" : undefined}
|
||||
info="Penalizes any token that has already appeared at least once, encouraging the model to introduce new topics. 0 = off."
|
||||
/>
|
||||
{!isExternalModel && !isGguf && (
|
||||
<ParamSlider
|
||||
label="Max Seq Length"
|
||||
|
|
|
|||
83
studio/frontend/src/features/chat/provider-capabilities.ts
Normal file
83
studio/frontend/src/features/chat/provider-capabilities.ts
Normal file
|
|
@ -0,0 +1,83 @@
|
|||
// SPDX-License-Identifier: AGPL-3.0-only
|
||||
// Copyright 2026-present the Unsloth AI Inc. team. All rights reserved. See /studio/LICENSE.AGPL-3.0
|
||||
|
||||
/**
|
||||
* Per-provider sampling parameter capability matrix.
|
||||
*
|
||||
* Values are derived from each provider's published chat-completion docs as of
|
||||
* 2026-05. They describe which of our UI knobs map cleanly onto the provider's
|
||||
* request body; the panel hides params a provider does not accept so users
|
||||
* cannot dial a value that gets silently dropped or rejected.
|
||||
*
|
||||
* "Local" models (anything that is not an external provider) are represented by
|
||||
* a null capability — every knob renders for them.
|
||||
*/
|
||||
|
||||
export interface ProviderCapabilities {
|
||||
/** top-k token sampling (only Anthropic on the providers we ship). */
|
||||
topK: boolean;
|
||||
/** min-p token cutoff (no SaaS provider currently exposes this). */
|
||||
minP: boolean;
|
||||
/** Repetition penalty (no SaaS provider currently exposes this). */
|
||||
repetitionPenalty: boolean;
|
||||
/** OpenAI-style presence penalty. */
|
||||
presencePenalty: boolean;
|
||||
}
|
||||
|
||||
const OPENAI_COMPAT_BASE: ProviderCapabilities = {
|
||||
topK: false,
|
||||
minP: false,
|
||||
repetitionPenalty: false,
|
||||
presencePenalty: true,
|
||||
};
|
||||
|
||||
const ALL_SUPPORTED: ProviderCapabilities = {
|
||||
topK: true,
|
||||
minP: true,
|
||||
repetitionPenalty: true,
|
||||
presencePenalty: true,
|
||||
};
|
||||
|
||||
const PROVIDER_CAPABILITIES: Record<string, ProviderCapabilities> = {
|
||||
openai: OPENAI_COMPAT_BASE,
|
||||
// Anthropic's Messages API accepts top_k but not presence/frequency penalty.
|
||||
anthropic: {
|
||||
topK: true,
|
||||
minP: false,
|
||||
repetitionPenalty: false,
|
||||
presencePenalty: false,
|
||||
},
|
||||
mistral: OPENAI_COMPAT_BASE,
|
||||
gemini: OPENAI_COMPAT_BASE,
|
||||
// DeepSeek deprecated presence/frequency penalty in their current docs.
|
||||
deepseek: {
|
||||
topK: false,
|
||||
minP: false,
|
||||
repetitionPenalty: false,
|
||||
presencePenalty: false,
|
||||
},
|
||||
kimi: OPENAI_COMPAT_BASE,
|
||||
qwen: OPENAI_COMPAT_BASE,
|
||||
huggingface: OPENAI_COMPAT_BASE,
|
||||
// OpenRouter silently drops params the target model does not support, so we
|
||||
// surface every knob and let the gateway handle the per-model fan-out.
|
||||
openrouter: ALL_SUPPORTED,
|
||||
// Custom providers are assumed OpenAI-compatible by the backend; users who
|
||||
// point at vLLM/Ollama backends often want top_k / min_p / repetition,
|
||||
// so be permissive.
|
||||
custom: ALL_SUPPORTED,
|
||||
};
|
||||
|
||||
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".
|
||||
*/
|
||||
export function getProviderCapabilities(
|
||||
providerType: string | null | undefined,
|
||||
): ProviderCapabilities | null {
|
||||
if (!providerType) return null;
|
||||
return PROVIDER_CAPABILITIES[providerType] ?? DEFAULT_EXTERNAL_CAPABILITIES;
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue