add reasoning slider (still need to hook it up)

This commit is contained in:
Andrew Pareles 2025-03-06 02:00:20 -08:00
parent 3bb9d5b3a5
commit d2e096957e
5 changed files with 223 additions and 51 deletions

View file

@ -17,7 +17,7 @@ import { ChatMarkdownRender, ChatMessageLocation } from '../markdown/ChatMarkdow
import { URI } from '../../../../../../../base/common/uri.js';
import { IDisposable } from '../../../../../../../base/common/lifecycle.js';
import { ErrorDisplay } from './ErrorDisplay.js';
import { TextAreaFns, VoidInputBox2 } from '../util/inputs.js';
import { TextAreaFns, VoidInputBox2, VoidSlider, VoidSwitch } from '../util/inputs.js';
import { ModelDropdown, } from '../void-settings-tsx/ModelDropdown.js';
import { SidebarThreadSelector } from './SidebarThreadSelector.js';
import { useScrollbarStyles } from '../util/useScrollbarStyles.js';
@ -29,7 +29,7 @@ import { WarningBox } from '../void-settings-tsx/WarningBox.js';
import { ChatMessage, StagingSelectionItem, ToolMessage } from '../../../chatThreadService.js';
import { filenameToVscodeLanguage } from '../../../../common/helpers/detectLanguage.js';
import { ToolName } from '../../../toolsService.js';
import { getModelCapabilities } from '../../../../common/modelCapabilities.js';
@ -146,6 +146,58 @@ export const IconLoading = ({ className = '' }: { className?: string }) => {
const getChatBubbleId = (threadId: string, messageIdx: number) => `${threadId}-${messageIdx}`;
const ReasoningOptionDropdown = () => {
const accessor = useAccessor()
const voidSettingsService = accessor.get('IVoidSettingsService')
const voidSettingsState = useSettingsState()
const modelSelection = voidSettingsState.modelSelectionOfFeature['Ctrl+L']
if (!modelSelection) return null
const { modelName, providerName } = modelSelection
const { canToggleReasoning, reasoningBudgetOptions } = getModelCapabilities(providerName, modelName).supportsReasoningOutput || {}
const defaultEnabledVal = canToggleReasoning ? true : false
const isEnabled = voidSettingsState.optionsOfModelSelection[modelSelection.providerName]?.[modelSelection.modelName]?.reasoningEnabled ?? defaultEnabledVal
let toggleButton: React.ReactNode = null
if (canToggleReasoning) {
toggleButton = <VoidSwitch
size='xs'
value={isEnabled}
onChange={(newVal) => { voidSettingsService.setOptionsOfModelSelection(modelSelection.providerName, modelSelection.modelName, { reasoningEnabled: newVal }) }}
/>
}
let slider: React.ReactNode = null
if (isEnabled && reasoningBudgetOptions?.type === 'slider') {
const { min, max, default: defaultVal } = reasoningBudgetOptions
const value = voidSettingsState.optionsOfModelSelection[modelSelection.providerName]?.[modelSelection.modelName]?.reasoningBudget ?? defaultVal
slider = <VoidSlider
width={50}
size='xs'
min={min}
max={max}
step={(max - min) / 8}
value={value}
onChange={(newVal) => { voidSettingsService.setOptionsOfModelSelection(modelSelection.providerName, modelSelection.modelName, { reasoningBudget: newVal }) }}
label={value + ''}
/>
}
return <>
{toggleButton}
{slider}
</>
}
interface VoidChatAreaProps {
// Required
children: React.ReactNode; // This will be the input component
@ -248,6 +300,8 @@ export const VoidChatArea: React.FC<VoidChatAreaProps> = ({
</div>
)}
<ReasoningOptionDropdown />
{isStreaming ? (
<ButtonStop onClick={onAbort} />
) : (

View file

@ -214,6 +214,151 @@ export const VoidInputBox = ({ onChangeText, onCreateInstance, inputBoxRef, plac
export const VoidSlider = ({
value,
onChange,
size = 'md',
label,
disabled = false,
min = 0,
max = 7,
step = 1,
className = '',
width = 200,
}: {
value: number;
onChange: (value: number) => void;
label?: string;
disabled?: boolean;
size?: 'xs' | 'sm' | 'sm+' | 'md';
min?: number;
max?: number;
step?: number;
className?: string;
width?: number;
}) => {
// Calculate percentage for position
const percentage = ((value - min) / (max - min)) * 100;
// Handle track click
const handleTrackClick = (e: React.MouseEvent<HTMLDivElement>) => {
if (disabled) return;
const rect = e.currentTarget.getBoundingClientRect();
const clickPosition = e.clientX - rect.left;
const trackWidth = rect.width;
// Calculate new value
const newPercentage = Math.max(0, Math.min(1, clickPosition / trackWidth));
const rawValue = min + newPercentage * (max - min);
// Special handling to ensure max value is always reachable
if (rawValue >= max - step / 2) {
onChange(max);
return;
}
// Normal step calculation
const steppedValue = Math.round((rawValue - min) / step) * step + min;
const clampedValue = Math.max(min, Math.min(max, steppedValue));
onChange(clampedValue);
};
// Helper function to handle thumb dragging that respects steps and max
const handleThumbDrag = (moveEvent: MouseEvent, track: Element) => {
if (!track) return;
const rect = (track as HTMLElement).getBoundingClientRect();
const movePosition = moveEvent.clientX - rect.left;
const trackWidth = rect.width;
// Calculate new value
const newPercentage = Math.max(0, Math.min(1, movePosition / trackWidth));
const rawValue = min + newPercentage * (max - min);
// Special handling to ensure max value is always reachable
if (rawValue >= max - step / 2) {
onChange(max);
return;
}
// Normal step calculation
const steppedValue = Math.round((rawValue - min) / step) * step + min;
const clampedValue = Math.max(min, Math.min(max, steppedValue));
onChange(clampedValue);
};
return (
<div className={`inline-flex items-center flex-shrink-0 ${className}`}>
<div className={`relative flex-shrink-0 ${disabled ? 'opacity-25' : ''}`} style={{ width }}>
{/* Track */}
<div
className={`relative ${size === 'xs' ? 'h-1' :
size === 'sm' ? 'h-1.5' :
size === 'sm+' ? 'h-2' : 'h-2.5'
} bg-gray-200 dark:bg-gray-700 rounded-full cursor-pointer`}
onClick={handleTrackClick}
>
{/* Filled part of track */}
<div
className={`absolute left-0 ${size === 'xs' ? 'h-1' :
size === 'sm' ? 'h-1.5' :
size === 'sm+' ? 'h-2' : 'h-2.5'
} bg-gray-900 dark:bg-white rounded-full`}
style={{ width: `${percentage}%` }}
/>
</div>
{/* Thumb */}
<div
className={`absolute top-1/2 transform -translate-x-1/2 -translate-y-1/2 ${size === 'xs' ? 'h-3 w-3' :
size === 'sm' ? 'h-4 w-4' :
size === 'sm+' ? 'h-5 w-5' : 'h-6 w-6'
} bg-white dark:bg-gray-900 rounded-full shadow-md ${disabled ? 'cursor-not-allowed' : 'cursor-grab active:cursor-grabbing'}`}
style={{ left: `${percentage}%` }}
onMouseDown={(e) => {
if (disabled) return;
const track = e.currentTarget.previousElementSibling;
const handleMouseMove = (moveEvent: MouseEvent) => {
handleThumbDrag(moveEvent, track as Element);
};
const handleMouseUp = () => {
document.removeEventListener('mousemove', handleMouseMove);
document.removeEventListener('mouseup', handleMouseUp);
document.body.style.cursor = '';
document.body.style.userSelect = '';
};
document.body.style.userSelect = 'none';
document.body.style.cursor = 'grabbing';
document.addEventListener('mousemove', handleMouseMove);
document.addEventListener('mouseup', handleMouseUp);
e.preventDefault();
}}
/>
</div>
{label && (
<span className={`ml-3 text-gray-900 dark:text-gray-100 ${size === 'xs' ? 'text-xs' : 'text-sm'
}`}>
{label}
</span>
)}
</div>
);
};
export const VoidSwitch = ({
value,
onChange,

View file

@ -21,7 +21,12 @@ type ModelOptions = {
supportsReasoningOutput: false | {
// you are allowed to not include openSourceThinkTags if it's not open source (no such cases as of writing)
// if it's open source, put the think tags here so we parse them out in e.g. ollama
readonly openSourceThinkTags?: [string, string]
readonly openSourceThinkTags?: [string, string];
// reasoning options
readonly canToggleReasoning?: boolean; // whether or not the user can enable reasoning mode (or if the model only supports reasoning)
readonly maxOutputTokens?: number;
readonly reasoningBudgetOptions?: { type: 'slider'; min: number; max: number; default: number };
};
}
@ -153,12 +158,16 @@ const extensiveModelFallback: ProviderSettings['modelOptionsFallback'] = (modelN
const anthropicModelOptions = {
'claude-3-7-sonnet-20250219': { // https://docs.anthropic.com/en/docs/about-claude/models/all-models#model-comparison-table
contextWindow: 200_000,
maxOutputTokens: 8_192, // TODO!!! 64_000 for extended thinking, can bump it to 128_000 with output-128k-2025-02-19
maxOutputTokens: 8_192,
cost: { input: 3.00, cache_read: 0.30, cache_write: 3.75, output: 15.00 },
supportsFIM: false,
supportsSystemMessage: 'separated',
supportsTools: 'anthropic-style',
supportsReasoningOutput: {},
supportsReasoningOutput: {
canToggleReasoning: true,
maxOutputTokens: 64_000, // can bump it to 128_000 with beta mode output-128k-2025-02-19
reasoningBudgetOptions: { type: 'slider', min: 1024, max: 32_000, default: 1024 }, // they recommend batching if max > 32_000
},
},
'claude-3-5-sonnet-20241022': {
contextWindow: 200_000,
@ -520,12 +529,12 @@ const modelSettingsOfProvider: { [providerName in ProviderName]: ProviderSetting
// ---------------- exports ----------------
export const getModelCapabilities = (providerName: ProviderName, modelName: string): ModelOptions & { modelName: string; didNotFindModel?: boolean } => {
export const getModelCapabilities = (providerName: ProviderName, modelName: string): ModelOptions & { modelName: string; isUnrecognizedModel: boolean } => {
const { modelOptions, modelOptionsFallback } = modelSettingsOfProvider[providerName]
if (modelName in modelOptions) return { modelName, ...modelOptions[modelName] }
if (modelName in modelOptions) return { modelName, ...modelOptions[modelName], isUnrecognizedModel: false }
const result = modelOptionsFallback(modelName)
if (result) return result
return { modelName, ...modelOptionsDefaults, didNotFindModel: true }
if (result) return { ...result, isUnrecognizedModel: false }
return { modelName, ...modelOptionsDefaults, isUnrecognizedModel: true }
}
// non-model settings

View file

@ -57,8 +57,6 @@ export interface IVoidSettingsService {
readonly state: VoidSettingsState; // in order to play nicely with react, you should immutably change state
readonly waitForInitState: Promise<void>;
readAndInitializeState: (providedState?: VoidSettingsState) => Promise<void>;
onDidChangeState: Event<void>;
setSettingOfProvider: SetSettingOfProviderFn;
@ -213,48 +211,13 @@ class VoidSettingsService extends Disposable implements IVoidSettingsService {
this.readAndInitializeState()
}
async readAndInitializeState(providedState?: VoidSettingsState) {
// If providedState is given, use it instead of reading from storage
const readS = providedState || await this._readState();
async readAndInitializeState() {
const readS = await this._readState();
// the stored data structure might be outdated, so we need to update it here
const newSettingsOfProvider = {
// A HACK BECAUSE WE ADDED DEEPSEEK (did not exist before, comes before readS)
...{ deepseek: defaultSettingsOfProvider.deepseek },
// A HACK BECAUSE WE ADDED XAI (did not exist before, comes before readS)
...{ xAI: defaultSettingsOfProvider.xAI },
// A HACK BECAUSE WE ADDED VLLM (did not exist before, comes before readS)
...{ vLLM: defaultSettingsOfProvider.vLLM },
...readS.settingsOfProvider,
// A HACK BECAUSE WE ADDED NEW GEMINI MODELS (existed before, comes after readS)
gemini: {
...readS.settingsOfProvider.gemini,
models: [
...readS.settingsOfProvider.gemini.models,
...defaultSettingsOfProvider.gemini.models.filter(m => /* if cant find the model in readS (yes this is O(n^2), very small) */ !readS.settingsOfProvider.gemini.models.find(m2 => m2.modelName === m.modelName))
]
}
};
const newModelSelectionOfFeature = {
// A HACK BECAUSE WE ADDED FastApply
...{ 'Apply': null },
...readS.modelSelectionOfFeature,
};
const finalState = {
...readS,
settingsOfProvider: newSettingsOfProvider,
modelSelectionOfFeature: newModelSelectionOfFeature,
};
const finalState = readS
this.state = _validatedState(finalState);
this._resolver();
this._onDidChangeState.fire();
}

View file

@ -462,7 +462,8 @@ export const globalSettingNames = Object.keys(defaultGlobalSettings) as GlobalSe
export type ModelSelectionOptions = {
reasoningSelection?: { budget: number }
reasoningEnabled?: boolean;
reasoningBudget?: number;
}
export type OptionsOfModelSelection = Partial<{ [providerName in ProviderName]: { [modelName: string]: ModelSelectionOptions } }>
export type OptionsOfModelSelection = Partial<{ [providerName in ProviderName]: { [modelName: string]: ModelSelectionOptions | undefined } }>