Add unified thinking effort control across all providers (#9242)
Signed-off-by: jh-block <jhugo@block.xyz> Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
+80
-1
@@ -1369,6 +1369,56 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"/config/providers/{name}/model-info": {
|
||||
"post": {
|
||||
"tags": [
|
||||
"super::routes::config_management"
|
||||
],
|
||||
"operationId": "get_provider_model_info",
|
||||
"parameters": [
|
||||
{
|
||||
"name": "name",
|
||||
"in": "path",
|
||||
"description": "Provider name (e.g., openai)",
|
||||
"required": true,
|
||||
"schema": {
|
||||
"type": "string"
|
||||
}
|
||||
}
|
||||
],
|
||||
"requestBody": {
|
||||
"content": {
|
||||
"application/json": {
|
||||
"schema": {
|
||||
"$ref": "#/components/schemas/ProviderModelInfoQuery"
|
||||
}
|
||||
}
|
||||
},
|
||||
"required": true
|
||||
},
|
||||
"responses": {
|
||||
"200": {
|
||||
"description": "Model metadata fetched successfully",
|
||||
"content": {
|
||||
"application/json": {
|
||||
"schema": {
|
||||
"$ref": "#/components/schemas/ModelInfo"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"400": {
|
||||
"description": "Unknown provider, provider not configured, or authentication error"
|
||||
},
|
||||
"429": {
|
||||
"description": "Rate limit exceeded"
|
||||
},
|
||||
"500": {
|
||||
"description": "Internal server error"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"/config/providers/{name}/models": {
|
||||
"get": {
|
||||
"tags": [
|
||||
@@ -1394,7 +1444,7 @@
|
||||
"schema": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
"type": "string"
|
||||
"$ref": "#/components/schemas/ModelInfo"
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -6533,6 +6583,10 @@
|
||||
"description": "Cost per token for output in USD (optional)",
|
||||
"nullable": true
|
||||
},
|
||||
"reasoning": {
|
||||
"type": "boolean",
|
||||
"description": "Whether this model supports reasoning/thinking controls"
|
||||
},
|
||||
"supports_cache_control": {
|
||||
"type": "boolean",
|
||||
"description": "Whether this model supports cache control",
|
||||
@@ -6546,6 +6600,7 @@
|
||||
"provider",
|
||||
"model",
|
||||
"context_limit",
|
||||
"reasoning",
|
||||
"currency"
|
||||
],
|
||||
"properties": {
|
||||
@@ -6586,6 +6641,9 @@
|
||||
},
|
||||
"provider": {
|
||||
"type": "string"
|
||||
},
|
||||
"reasoning": {
|
||||
"type": "boolean"
|
||||
}
|
||||
}
|
||||
},
|
||||
@@ -6973,6 +7031,17 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"ProviderModelInfoQuery": {
|
||||
"type": "object",
|
||||
"required": [
|
||||
"model"
|
||||
],
|
||||
"properties": {
|
||||
"model": {
|
||||
"type": "string"
|
||||
}
|
||||
}
|
||||
},
|
||||
"ProviderTemplate": {
|
||||
"type": "object",
|
||||
"required": [
|
||||
@@ -8568,6 +8637,16 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"ThinkingEffort": {
|
||||
"type": "string",
|
||||
"enum": [
|
||||
"off",
|
||||
"low",
|
||||
"medium",
|
||||
"high",
|
||||
"max"
|
||||
]
|
||||
},
|
||||
"TokenState": {
|
||||
"type": "object",
|
||||
"required": [
|
||||
|
||||
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
@@ -818,6 +818,10 @@ export type ModelInfo = {
|
||||
* Cost per token for output in USD (optional)
|
||||
*/
|
||||
output_token_cost?: number | null;
|
||||
/**
|
||||
* Whether this model supports reasoning/thinking controls
|
||||
*/
|
||||
reasoning?: boolean;
|
||||
/**
|
||||
* Whether this model supports cache control
|
||||
*/
|
||||
@@ -834,6 +838,7 @@ export type ModelInfoData = {
|
||||
model: string;
|
||||
output_token_cost?: number | null;
|
||||
provider: string;
|
||||
reasoning: boolean;
|
||||
};
|
||||
|
||||
export type ModelInfoQuery = {
|
||||
@@ -1000,6 +1005,10 @@ export type ProviderMetadata = {
|
||||
setup_steps?: Array<string>;
|
||||
};
|
||||
|
||||
export type ProviderModelInfoQuery = {
|
||||
model: string;
|
||||
};
|
||||
|
||||
export type ProviderTemplate = {
|
||||
api_url: string;
|
||||
doc_url: string;
|
||||
@@ -1482,6 +1491,8 @@ export type ThinkingContent = {
|
||||
thinking: string;
|
||||
};
|
||||
|
||||
export type ThinkingEffort = 'off' | 'low' | 'medium' | 'high' | 'max';
|
||||
|
||||
export type TokenState = {
|
||||
accumulatedCost?: number | null;
|
||||
accumulatedInputTokens: number;
|
||||
@@ -2728,6 +2739,42 @@ export type CleanupProviderCacheResponses = {
|
||||
|
||||
export type CleanupProviderCacheResponse = CleanupProviderCacheResponses[keyof CleanupProviderCacheResponses];
|
||||
|
||||
export type GetProviderModelInfoData = {
|
||||
body: ProviderModelInfoQuery;
|
||||
path: {
|
||||
/**
|
||||
* Provider name (e.g., openai)
|
||||
*/
|
||||
name: string;
|
||||
};
|
||||
query?: never;
|
||||
url: '/config/providers/{name}/model-info';
|
||||
};
|
||||
|
||||
export type GetProviderModelInfoErrors = {
|
||||
/**
|
||||
* Unknown provider, provider not configured, or authentication error
|
||||
*/
|
||||
400: unknown;
|
||||
/**
|
||||
* Rate limit exceeded
|
||||
*/
|
||||
429: unknown;
|
||||
/**
|
||||
* Internal server error
|
||||
*/
|
||||
500: unknown;
|
||||
};
|
||||
|
||||
export type GetProviderModelInfoResponses = {
|
||||
/**
|
||||
* Model metadata fetched successfully
|
||||
*/
|
||||
200: ModelInfo;
|
||||
};
|
||||
|
||||
export type GetProviderModelInfoResponse = GetProviderModelInfoResponses[keyof GetProviderModelInfoResponses];
|
||||
|
||||
export type GetProviderModelsData = {
|
||||
body?: never;
|
||||
path: {
|
||||
@@ -2759,7 +2806,7 @@ export type GetProviderModelsResponses = {
|
||||
/**
|
||||
* Models fetched successfully
|
||||
*/
|
||||
200: Array<string>;
|
||||
200: Array<ModelInfo>;
|
||||
};
|
||||
|
||||
export type GetProviderModelsResponse = GetProviderModelsResponses[keyof GetProviderModelsResponses];
|
||||
|
||||
@@ -108,8 +108,8 @@ export const RecipeModelSelector = ({
|
||||
|
||||
const modelList = models || [];
|
||||
const options = modelList.map((m) => ({
|
||||
value: m,
|
||||
label: m,
|
||||
value: m.name,
|
||||
label: m.name,
|
||||
provider: p.name,
|
||||
}));
|
||||
|
||||
|
||||
@@ -1,4 +1,10 @@
|
||||
import { ProviderDetails, getProviderModels, listLocalModels } from '../../../api';
|
||||
import {
|
||||
ProviderDetails,
|
||||
ThinkingEffort,
|
||||
getProviderModelInfo,
|
||||
getProviderModels,
|
||||
listLocalModels,
|
||||
} from '../../../api';
|
||||
import { errorMessage as getErrorMessage } from '../../../utils/conversionUtils';
|
||||
|
||||
export default interface Model {
|
||||
@@ -9,7 +15,8 @@ export default interface Model {
|
||||
alias?: string; // optional model display name
|
||||
subtext?: string; // goes below model name if not the provider
|
||||
context_limit?: number; // optional context limit override
|
||||
request_params?: Record<string, unknown>; // provider-specific request parameters
|
||||
reasoning?: boolean; // optional reasoning/thinking support metadata
|
||||
request_params?: Record<string, unknown> & { thinking_effort?: ThinkingEffort }; // provider-specific request parameters
|
||||
}
|
||||
|
||||
export function createModelStruct(
|
||||
@@ -45,7 +52,7 @@ export async function getProviderMetadata(
|
||||
|
||||
export interface ProviderModelsResult {
|
||||
provider: ProviderDetails;
|
||||
models: string[] | null;
|
||||
models: Model[] | null;
|
||||
error: string | null;
|
||||
warning: string | null;
|
||||
}
|
||||
@@ -61,7 +68,7 @@ export async function fetchModelsForProviders(
|
||||
const allModels = response.data || [];
|
||||
const downloadedModels = allModels
|
||||
.filter((m) => m.status.state === 'Downloaded')
|
||||
.map((m) => m.id);
|
||||
.map((m) => ({ name: m.id, provider: p.name }) as Model);
|
||||
return { provider: p, models: downloadedModels, error: null, warning: null };
|
||||
}
|
||||
|
||||
@@ -69,12 +76,28 @@ export async function fetchModelsForProviders(
|
||||
path: { name: p.name },
|
||||
throwOnError: true,
|
||||
});
|
||||
const models = response.data || [];
|
||||
const models = (response.data || []).map(
|
||||
(m) =>
|
||||
({
|
||||
name: m.name,
|
||||
provider: p.name,
|
||||
context_limit: m.context_limit,
|
||||
reasoning: m.reasoning ?? undefined,
|
||||
}) as Model
|
||||
);
|
||||
return { provider: p, models, error: null, warning: null };
|
||||
} catch (e: unknown) {
|
||||
// For custom providers, fall back to the configured model list
|
||||
if (p.provider_type === 'Custom') {
|
||||
const fallbackModels = p.metadata.known_models.map((m) => m.name);
|
||||
const fallbackModels = p.metadata.known_models.map(
|
||||
(m) =>
|
||||
({
|
||||
name: m.name,
|
||||
provider: p.name,
|
||||
context_limit: m.context_limit,
|
||||
reasoning: m.reasoning ?? undefined,
|
||||
}) as Model
|
||||
);
|
||||
if (fallbackModels.length > 0) {
|
||||
console.warn(`Failed to fetch models for ${p.name}:`, getErrorMessage(e));
|
||||
return {
|
||||
@@ -99,3 +122,19 @@ export async function fetchModelsForProviders(
|
||||
|
||||
return await Promise.all(modelPromises);
|
||||
}
|
||||
|
||||
export async function fetchModelReasoning(
|
||||
provider: string,
|
||||
model: string,
|
||||
fallback?: boolean
|
||||
): Promise<boolean | null> {
|
||||
try {
|
||||
const response = await getProviderModelInfo({
|
||||
path: { name: provider },
|
||||
body: { model },
|
||||
});
|
||||
return response.data?.reasoning ?? fallback ?? null;
|
||||
} catch {
|
||||
return fallback ?? null;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -17,12 +17,20 @@ import { Select } from '../../../ui/Select';
|
||||
import { useConfig } from '../../../ConfigContext';
|
||||
import { useModelAndProvider } from '../../../ModelAndProviderContext';
|
||||
import type { View } from '../../../../utils/navigationUtils';
|
||||
import Model, { getProviderMetadata, fetchModelsForProviders } from '../modelInterface';
|
||||
import Model, {
|
||||
fetchModelReasoning,
|
||||
fetchModelsForProviders,
|
||||
getProviderMetadata,
|
||||
} from '../modelInterface';
|
||||
import { getPredefinedModelsFromEnv, shouldShowPredefinedModels } from '../predefinedModelsUtils';
|
||||
import { ProviderType } from '../../../../api';
|
||||
import type { ProviderType, ThinkingEffort } from '../../../../api';
|
||||
import { trackModelChanged } from '../../../../utils/analytics';
|
||||
|
||||
const i18n = defineMessages({
|
||||
thinkingEffortOff: {
|
||||
id: 'switchModelModal.thinkingEffortOff',
|
||||
defaultMessage: 'Off - No extended thinking',
|
||||
},
|
||||
thinkingLevelLow: {
|
||||
id: 'switchModelModal.thinkingLevelLow',
|
||||
defaultMessage: 'Low - Better latency, lighter reasoning',
|
||||
@@ -185,16 +193,7 @@ const i18n = defineMessages({
|
||||
},
|
||||
});
|
||||
|
||||
// THINKING_LEVEL_OPTIONS and CLAUDE_THINKING_EFFORT_OPTIONS are created inside the component to support i18n.
|
||||
|
||||
function isClaudeModel(name: string | null | undefined): boolean {
|
||||
return !!name && name.toLowerCase().startsWith('claude-');
|
||||
}
|
||||
|
||||
function supportsAdaptiveThinking(name: string): boolean {
|
||||
const lower = name.toLowerCase();
|
||||
return lower.includes('claude-opus-4-6') || lower.includes('claude-sonnet-4-6');
|
||||
}
|
||||
// Thinking effort options are created inside the component to support i18n.
|
||||
|
||||
const PREFERRED_MODEL_PATTERNS = [
|
||||
/claude-sonnet-4/i,
|
||||
@@ -256,12 +255,8 @@ export const SwitchModelModal = ({
|
||||
}: SwitchModelModalProps) => {
|
||||
const intl = useIntl();
|
||||
|
||||
const THINKING_LEVEL_OPTIONS = [
|
||||
{ value: 'low', label: intl.formatMessage(i18n.thinkingLevelLow) },
|
||||
{ value: 'high', label: intl.formatMessage(i18n.thinkingLevelHigh) },
|
||||
];
|
||||
|
||||
const CLAUDE_THINKING_EFFORT_OPTIONS = [
|
||||
const THINKING_EFFORT_OPTIONS: { value: ThinkingEffort; label: string }[] = [
|
||||
{ value: 'off', label: intl.formatMessage(i18n.thinkingEffortOff) },
|
||||
{ value: 'low', label: intl.formatMessage(i18n.claudeEffortLow) },
|
||||
{ value: 'medium', label: intl.formatMessage(i18n.claudeEffortMedium) },
|
||||
{ value: 'high', label: intl.formatMessage(i18n.claudeEffortHigh) },
|
||||
@@ -278,7 +273,13 @@ export const SwitchModelModal = ({
|
||||
const currentModel = sessionModel ?? configModel;
|
||||
const currentProvider = sessionProvider ?? configProvider;
|
||||
const [providerOptions, setProviderOptions] = useState<{ value: string; label: string }[]>([]);
|
||||
type ModelOption = { value: string; label: string; provider: string; isDisabled?: boolean };
|
||||
type ModelOption = {
|
||||
value: string;
|
||||
label: string;
|
||||
provider: string;
|
||||
isDisabled?: boolean;
|
||||
reasoning?: boolean;
|
||||
};
|
||||
const [modelOptions, setModelOptions] = useState<{ options: ModelOption[] }[]>([]);
|
||||
const [provider, setProvider] = useState<string | null>(
|
||||
initialProvider || currentProvider || null
|
||||
@@ -304,43 +305,56 @@ export const SwitchModelModal = ({
|
||||
import('../../../../api').ProviderDetails[]
|
||||
>([]);
|
||||
const fetchedProviders = useRef<Set<string>>(new Set());
|
||||
const [thinkingLevel, setThinkingLevel] = useState<string>('low');
|
||||
const [claudeThinkingType, setClaudeThinkingType] = useState<string>('disabled');
|
||||
const [claudeThinkingEffort, setClaudeThinkingEffort] = useState<string>('high');
|
||||
const [claudeThinkingBudget, setClaudeThinkingBudget] = useState<string>('16000');
|
||||
const reasoningRequestId = useRef(0);
|
||||
const [thinkingEffort, setThinkingEffort] = useState<ThinkingEffort | null>(null);
|
||||
const [selectedModelReasoning, setSelectedModelReasoning] = useState<boolean | null>(null);
|
||||
|
||||
const modelName = usePredefinedModels ? selectedPredefinedModel?.name : model;
|
||||
const isGemini3Model = modelName?.toLowerCase().startsWith('gemini-3') ?? false;
|
||||
const showClaudeThinking = isClaudeModel(modelName);
|
||||
const modelSupportsAdaptive = modelName ? supportsAdaptiveThinking(modelName) : false;
|
||||
const modelReasoning = selectedModelReasoning ?? selectedPredefinedModel?.reasoning;
|
||||
const showThinkingControl = modelReasoning === true;
|
||||
const resolveSelectedModelReasoning = useCallback(
|
||||
(providerName: string, modelName: string, fallback?: boolean) => {
|
||||
const requestId = ++reasoningRequestId.current;
|
||||
setSelectedModelReasoning(fallback ?? null);
|
||||
fetchModelReasoning(providerName, modelName, fallback).then((reasoning) => {
|
||||
if (requestId === reasoningRequestId.current) {
|
||||
setSelectedModelReasoning(reasoning);
|
||||
}
|
||||
});
|
||||
},
|
||||
[]
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
if (!showClaudeThinking) return;
|
||||
if (claudeThinkingType === 'adaptive' && !modelSupportsAdaptive) {
|
||||
setClaudeThinkingType('disabled');
|
||||
}
|
||||
}, [modelName, showClaudeThinking, modelSupportsAdaptive, claudeThinkingType]);
|
||||
|
||||
useEffect(() => {
|
||||
const readConfig = async (key: string): Promise<string | null> => {
|
||||
try {
|
||||
const val = (await read(key, false)) as string;
|
||||
return val || null;
|
||||
} catch (e) {
|
||||
console.warn(`Could not read ${key}, using default:`, e);
|
||||
return null;
|
||||
}
|
||||
};
|
||||
(async () => {
|
||||
const tt = await readConfig('CLAUDE_THINKING_TYPE');
|
||||
if (tt) setClaudeThinkingType(tt);
|
||||
const effort = await readConfig('CLAUDE_THINKING_EFFORT');
|
||||
if (effort) setClaudeThinkingEffort(effort);
|
||||
const budget = await readConfig('CLAUDE_THINKING_BUDGET');
|
||||
if (budget) setClaudeThinkingBudget(budget);
|
||||
try {
|
||||
const effort = (await read('GOOSE_THINKING_EFFORT', false)) as ThinkingEffort;
|
||||
if (effort) setThinkingEffort(effort);
|
||||
} catch (e) {
|
||||
console.warn('Could not read GOOSE_THINKING_EFFORT, using default:', e);
|
||||
}
|
||||
})();
|
||||
}, [read]);
|
||||
|
||||
useEffect(() => {
|
||||
if (!provider || !model) return;
|
||||
|
||||
const selectedOption = modelOptions
|
||||
.flatMap((group) => group.options)
|
||||
.find((option) => option.provider === provider && option.value === model);
|
||||
|
||||
if (selectedOption) {
|
||||
resolveSelectedModelReasoning(provider, model, selectedOption.reasoning);
|
||||
return;
|
||||
}
|
||||
|
||||
setSelectedModelReasoning(null);
|
||||
const timeout = setTimeout(() => {
|
||||
resolveSelectedModelReasoning(provider, model);
|
||||
}, 400);
|
||||
|
||||
return () => clearTimeout(timeout);
|
||||
}, [model, provider, modelOptions, resolveSelectedModelReasoning]);
|
||||
|
||||
// Validate form data
|
||||
const validateForm = useCallback(() => {
|
||||
const errors = {
|
||||
@@ -393,36 +407,18 @@ export const SwitchModelModal = ({
|
||||
subtext: providerDisplayName,
|
||||
} as Model;
|
||||
}
|
||||
modelObj = {
|
||||
...modelObj,
|
||||
reasoning: selectedModelReasoning ?? modelObj.reasoning,
|
||||
};
|
||||
|
||||
if (isGemini3Model) {
|
||||
if (showThinkingControl) {
|
||||
const effort = thinkingEffort ?? modelObj.request_params?.thinking_effort ?? 'off';
|
||||
modelObj = {
|
||||
...modelObj,
|
||||
request_params: { ...modelObj.request_params, thinking_level: thinkingLevel },
|
||||
request_params: { ...modelObj.request_params, thinking_effort: effort },
|
||||
};
|
||||
}
|
||||
|
||||
if (showClaudeThinking) {
|
||||
const params: Record<string, unknown> = {
|
||||
...modelObj.request_params,
|
||||
thinking_type: claudeThinkingType,
|
||||
};
|
||||
if (claudeThinkingType === 'adaptive') {
|
||||
params.effort = claudeThinkingEffort;
|
||||
} else if (claudeThinkingType === 'enabled') {
|
||||
params.budget_tokens = parseInt(claudeThinkingBudget, 10) || 16000;
|
||||
}
|
||||
modelObj = { ...modelObj, request_params: params };
|
||||
|
||||
upsert('CLAUDE_THINKING_TYPE', claudeThinkingType, false).catch(console.warn);
|
||||
if (claudeThinkingType === 'adaptive') {
|
||||
upsert('CLAUDE_THINKING_EFFORT', claudeThinkingEffort, false).catch(console.warn);
|
||||
} else if (claudeThinkingType === 'enabled') {
|
||||
upsert(
|
||||
'CLAUDE_THINKING_BUDGET',
|
||||
parseInt(claudeThinkingBudget, 10) || 16000,
|
||||
false
|
||||
).catch(console.warn);
|
||||
}
|
||||
upsert('GOOSE_THINKING_EFFORT', effort, false).catch(console.warn);
|
||||
}
|
||||
|
||||
const success = await changeModel(sessionId, modelObj);
|
||||
@@ -450,8 +446,13 @@ export const SwitchModelModal = ({
|
||||
const matchingModel = models.find((m) => m.name === currentModel);
|
||||
if (matchingModel) {
|
||||
setSelectedPredefinedModel(matchingModel);
|
||||
resolveSelectedModelReasoning(
|
||||
matchingModel.provider,
|
||||
matchingModel.name,
|
||||
matchingModel.reasoning
|
||||
);
|
||||
}
|
||||
}, [usePredefinedModels, currentModel]);
|
||||
}, [usePredefinedModels, currentModel, resolveSelectedModelReasoning]);
|
||||
|
||||
// For manual mode: one-time sync of provider/model when session data
|
||||
// arrives after the modal has already mounted. Uses a ref so it only
|
||||
@@ -515,7 +516,7 @@ export const SwitchModelModal = ({
|
||||
if (cancelled) return;
|
||||
|
||||
const newGroupedOptions: {
|
||||
options: { value: string; label: string; provider: string; providerType: ProviderType }[];
|
||||
options: (ModelOption & { providerType: ProviderType })[];
|
||||
}[] = [];
|
||||
const newErrors: Record<string, string> = {};
|
||||
const newWarnings: Record<string, string> = {};
|
||||
@@ -536,11 +537,13 @@ export const SwitchModelModal = ({
|
||||
label: string;
|
||||
provider: string;
|
||||
providerType: ProviderType;
|
||||
reasoning?: boolean;
|
||||
}[] = modelList.map((m) => ({
|
||||
value: m,
|
||||
label: m,
|
||||
value: m.name,
|
||||
label: m.name,
|
||||
provider: p.name,
|
||||
providerType: p.provider_type,
|
||||
reasoning: m.reasoning,
|
||||
}));
|
||||
|
||||
if (p.provider_type !== 'Custom') {
|
||||
@@ -613,30 +616,51 @@ export const SwitchModelModal = ({
|
||||
}
|
||||
}, [provider, modelOptions, loadingModels, model, isCustomModel, userClearedModel, activeProvidersList]);
|
||||
|
||||
const handlePredefinedModelChange = (model: Model) => {
|
||||
setSelectedPredefinedModel(model);
|
||||
resolveSelectedModelReasoning(model.provider, model.name, model.reasoning);
|
||||
};
|
||||
|
||||
// Handle model selection change
|
||||
const handleModelChange = (newValue: unknown) => {
|
||||
const selectedOption = newValue as { value: string; label: string; provider: string } | null;
|
||||
const selectedOption = newValue as {
|
||||
value: string;
|
||||
label: string;
|
||||
provider: string;
|
||||
reasoning?: boolean;
|
||||
} | null;
|
||||
if (selectedOption?.value === 'custom') {
|
||||
setIsCustomModel(true);
|
||||
setModel('');
|
||||
setProvider(selectedOption.provider);
|
||||
setSelectedModelReasoning(null);
|
||||
setUserClearedModel(false);
|
||||
} else if (selectedOption === null) {
|
||||
// User cleared the selection
|
||||
setIsCustomModel(false);
|
||||
setModel('');
|
||||
setSelectedModelReasoning(null);
|
||||
setUserClearedModel(true);
|
||||
} else {
|
||||
setIsCustomModel(false);
|
||||
setModel(selectedOption?.value || '');
|
||||
setProvider(selectedOption?.provider || '');
|
||||
if (selectedOption?.provider && selectedOption.value) {
|
||||
resolveSelectedModelReasoning(
|
||||
selectedOption.provider,
|
||||
selectedOption.value,
|
||||
selectedOption.reasoning
|
||||
);
|
||||
} else {
|
||||
setSelectedModelReasoning(selectedOption?.reasoning ?? null);
|
||||
}
|
||||
setUserClearedModel(false);
|
||||
}
|
||||
};
|
||||
|
||||
// Store the original model options in state, initialized from modelOptions
|
||||
const [originalModelOptions, setOriginalModelOptions] =
|
||||
useState<{ options: { value: string; label: string; provider: string }[] }[]>(modelOptions);
|
||||
useState<{ options: ModelOption[] }[]>(modelOptions);
|
||||
|
||||
const handleInputChange = (inputValue: string) => {
|
||||
if (!provider) return;
|
||||
@@ -680,54 +704,20 @@ export const SwitchModelModal = ({
|
||||
}
|
||||
};
|
||||
|
||||
const claudeThinkingTypeOptions = [
|
||||
...(modelSupportsAdaptive
|
||||
? [{ value: 'adaptive', label: intl.formatMessage(i18n.claudeAdaptive) }]
|
||||
: []),
|
||||
{ value: 'enabled', label: intl.formatMessage(i18n.claudeEnabled) },
|
||||
{ value: 'disabled', label: intl.formatMessage(i18n.claudeDisabled) },
|
||||
];
|
||||
|
||||
const claudeThinkingControls = showClaudeThinking && (
|
||||
<div className="mt-2 flex flex-col gap-3">
|
||||
<div>
|
||||
<label className="text-sm text-textSubtle mb-1 block">{intl.formatMessage(i18n.extendedThinking)}</label>
|
||||
<Select
|
||||
options={claudeThinkingTypeOptions}
|
||||
value={claudeThinkingTypeOptions.find((o) => o.value === claudeThinkingType)}
|
||||
onChange={(newValue: unknown) => {
|
||||
const option = newValue as { value: string; label: string } | null;
|
||||
setClaudeThinkingType(option?.value || 'disabled');
|
||||
}}
|
||||
placeholder={intl.formatMessage(i18n.selectThinkingMode)}
|
||||
/>
|
||||
</div>
|
||||
{claudeThinkingType === 'adaptive' && (
|
||||
<div>
|
||||
<label className="text-sm text-textSubtle mb-1 block">{intl.formatMessage(i18n.thinkingEffort)}</label>
|
||||
<Select
|
||||
options={CLAUDE_THINKING_EFFORT_OPTIONS}
|
||||
value={CLAUDE_THINKING_EFFORT_OPTIONS.find((o) => o.value === claudeThinkingEffort)}
|
||||
onChange={(newValue: unknown) => {
|
||||
const option = newValue as { value: string; label: string } | null;
|
||||
setClaudeThinkingEffort(option?.value || 'high');
|
||||
}}
|
||||
placeholder={intl.formatMessage(i18n.selectEffortLevel)}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
{claudeThinkingType === 'enabled' && (
|
||||
<div>
|
||||
<label className="text-sm text-textSubtle mb-1 block">{intl.formatMessage(i18n.thinkingBudget)}</label>
|
||||
<Input
|
||||
className="border-2 px-4 py-2"
|
||||
type="number"
|
||||
min="1024"
|
||||
value={claudeThinkingBudget}
|
||||
onChange={(e) => setClaudeThinkingBudget(e.target.value)}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
const thinkingEffortControl = showThinkingControl && (
|
||||
<div className="mt-2">
|
||||
<label className="text-sm text-textSubtle mb-1 block">
|
||||
{intl.formatMessage(i18n.thinkingEffort)}
|
||||
</label>
|
||||
<Select
|
||||
options={THINKING_EFFORT_OPTIONS}
|
||||
value={THINKING_EFFORT_OPTIONS.find((o) => o.value === (thinkingEffort ?? 'off'))}
|
||||
onChange={(newValue: unknown) => {
|
||||
const option = newValue as { value: ThinkingEffort; label: string } | null;
|
||||
setThinkingEffort(option?.value || 'off');
|
||||
}}
|
||||
placeholder={intl.formatMessage(i18n.selectEffortLevel)}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
|
||||
@@ -760,7 +750,7 @@ export const SwitchModelModal = ({
|
||||
? 'bg-background-secondary'
|
||||
: 'bg-background-primary hover:bg-background-secondary'
|
||||
} rounded-lg transition-all`}
|
||||
onClick={() => setSelectedPredefinedModel(model)}
|
||||
onClick={() => handlePredefinedModelChange(model)}
|
||||
>
|
||||
<div className="flex-1">
|
||||
<div className="flex items-center justify-between">
|
||||
@@ -786,7 +776,7 @@ export const SwitchModelModal = ({
|
||||
name="predefined-model"
|
||||
value={model.name}
|
||||
checked={selectedPredefinedModel?.name === model.name}
|
||||
onChange={() => setSelectedPredefinedModel(model)}
|
||||
onChange={() => handlePredefinedModelChange(model)}
|
||||
className="peer sr-only"
|
||||
/>
|
||||
<div
|
||||
@@ -805,25 +795,7 @@ export const SwitchModelModal = ({
|
||||
<div className="text-red-500 text-sm mt-1">{validationErrors.model}</div>
|
||||
)}
|
||||
|
||||
{isGemini3Model && (
|
||||
<div className="mt-2">
|
||||
<label className="text-sm text-textSubtle mb-1 block">
|
||||
{intl.formatMessage(i18n.thinkingLevel)}
|
||||
<span className="text-xs text-textMuted ml-2">{intl.formatMessage(i18n.geminiOnly)}</span>
|
||||
</label>
|
||||
<Select
|
||||
options={THINKING_LEVEL_OPTIONS}
|
||||
value={THINKING_LEVEL_OPTIONS.find((o) => o.value === thinkingLevel)}
|
||||
onChange={(newValue: unknown) => {
|
||||
const option = newValue as { value: string; label: string } | null;
|
||||
setThinkingLevel(option?.value || 'low');
|
||||
}}
|
||||
placeholder={intl.formatMessage(i18n.selectThinkingLevel)}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{claudeThinkingControls}
|
||||
{thinkingEffortControl}
|
||||
</div>
|
||||
) : (
|
||||
/* Manual Provider/Model Selection */
|
||||
@@ -970,25 +942,7 @@ export const SwitchModelModal = ({
|
||||
</div>
|
||||
)}
|
||||
|
||||
{isGemini3Model && (
|
||||
<div className="mt-2">
|
||||
<label className="text-sm text-textSubtle mb-1 block">
|
||||
Thinking Level
|
||||
<span className="text-xs text-textMuted ml-2">(Gemini 3 models only)</span>
|
||||
</label>
|
||||
<Select
|
||||
options={THINKING_LEVEL_OPTIONS}
|
||||
value={THINKING_LEVEL_OPTIONS.find((o) => o.value === thinkingLevel)}
|
||||
onChange={(newValue: unknown) => {
|
||||
const option = newValue as { value: string; label: string } | null;
|
||||
setThinkingLevel(option?.value || 'low');
|
||||
}}
|
||||
placeholder="Select thinking level"
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{claudeThinkingControls}
|
||||
{thinkingEffortControl}
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
|
||||
@@ -4475,6 +4475,9 @@
|
||||
"switchModelModal.thinkingEffort": {
|
||||
"defaultMessage": "Thinking Effort"
|
||||
},
|
||||
"switchModelModal.thinkingEffortOff": {
|
||||
"defaultMessage": "Off - No extended thinking"
|
||||
},
|
||||
"switchModelModal.thinkingLevel": {
|
||||
"defaultMessage": "Thinking Level"
|
||||
},
|
||||
|
||||
Reference in New Issue
Block a user