import chalk from 'chalk'; import * as React from 'react'; import type { CommandResultDisplay } from '../../commands.js'; import { ModelPicker } from '../../components/ModelPicker.js'; import { COMMON_HELP_ARGS, COMMON_INFO_ARGS } from '../../constants/xml.js'; import { type AnalyticsMetadata_I_VERIFIED_THIS_IS_NOT_CODE_OR_FILEPATHS, logEvent, } from '../../services/analytics/index.js'; import { useAppState, useSetAppState } from '../../state/AppState.js'; import type { LocalJSXCommandCall } from '../../types/command.js'; import type { EffortLevel } from '../../utils/effort.js'; import { isBilledAsExtraUsage } from '../../utils/extraUsage.js'; import { clearFastModeCooldown, isFastModeAvailable, isFastModeEnabled, isFastModeSupportedByModel, } from '../../utils/fastMode.js'; import { MODEL_ALIASES } from '../../utils/model/aliases.js'; import { checkOpus1mAccess, checkSonnet1mAccess } from '../../utils/model/check1mAccess.js'; import { getDefaultMainLoopModelSetting, isOpus1mMergeEnabled, renderDefaultModelSetting, } from '../../utils/model/model.js'; import { isModelAllowed } from '../../utils/model/modelAllowlist.js'; import { validateModel } from '../../utils/model/validateModel.js'; function ModelPickerWrapper({ onDone, }: { onDone: (result?: string, options?: { display?: CommandResultDisplay }) => void; }): React.ReactNode { const mainLoopModel = useAppState(s => s.mainLoopModel); const mainLoopModelForSession = useAppState(s => s.mainLoopModelForSession); const isFastMode = useAppState(s => s.fastMode); const setAppState = useSetAppState(); function handleCancel(): void { logEvent('tengu_model_command_menu', { action: 'cancel' as AnalyticsMetadata_I_VERIFIED_THIS_IS_NOT_CODE_OR_FILEPATHS, }); const displayModel = renderModelLabel(mainLoopModel); onDone(`Kept model as ${chalk.bold(displayModel)}`, { display: 'system', }); } function handleSelect(model: string | null, effort: EffortLevel | undefined): void { logEvent('tengu_model_command_menu', { action: model as AnalyticsMetadata_I_VERIFIED_THIS_IS_NOT_CODE_OR_FILEPATHS, from_model: mainLoopModel as AnalyticsMetadata_I_VERIFIED_THIS_IS_NOT_CODE_OR_FILEPATHS, to_model: model as AnalyticsMetadata_I_VERIFIED_THIS_IS_NOT_CODE_OR_FILEPATHS, }); setAppState(prev => ({ ...prev, mainLoopModel: model, mainLoopModelForSession: null, })); let message = `Set model to ${chalk.bold(renderModelLabel(model))}`; if (effort !== undefined) { message += ` with ${chalk.bold(effort)} effort`; } // Turn off fast mode if switching to unsupported model let wasFastModeToggledOn; if (isFastModeEnabled()) { clearFastModeCooldown(); if (!isFastModeSupportedByModel(model) && isFastMode) { setAppState(prev => ({ ...prev, fastMode: false, })); wasFastModeToggledOn = false; // Do not update fast mode in settings since this is an automatic downgrade } else if (isFastModeSupportedByModel(model) && isFastModeAvailable() && isFastMode) { message += ` · Fast mode ON`; wasFastModeToggledOn = true; } } if (isBilledAsExtraUsage(model, wasFastModeToggledOn === true, isOpus1mMergeEnabled())) { message += ` · Billed as extra usage`; } if (wasFastModeToggledOn === false) { // Fast mode was toggled off, show suffix after extra usage billing message += ` · Fast mode OFF`; } onDone(message); } return ( ); } function SetModelAndClose({ args, onDone, }: { args: string; onDone: (result?: string, options?: { display?: CommandResultDisplay }) => void; }): React.ReactNode { const isFastMode = useAppState(s => s.fastMode); const setAppState = useSetAppState(); const model = args === 'default' ? null : args; React.useEffect(() => { async function handleModelChange(): Promise { if (model && !isModelAllowed(model)) { onDone(`Model '${model}' is not available. Your organization restricts model selection.`, { display: 'system', }); return; } // @[MODEL LAUNCH]: Update check for 1M access. if (model && isOpus1mUnavailable(model)) { onDone( `Opus 4.7 with 1M context is not available for your account. Learn more: https://code.claude.com/docs/en/model-config#extended-context-with-1m`, { display: 'system' }, ); return; } if (model && isSonnet1mUnavailable(model)) { onDone( `Sonnet 4.6 with 1M context is not available for your account. Learn more: https://code.claude.com/docs/en/model-config#extended-context-with-1m`, { display: 'system' }, ); return; } // Skip validation for default model if (!model) { setModel(null); return; } // Skip validation for known aliases - they're predefined and should work if (isKnownAlias(model)) { setModel(model); return; } // Validate and set custom model try { // Don't use parseUserSpecifiedModel for non-aliases since it lowercases the input // and model names are case-sensitive const { valid, error } = await validateModel(model); if (valid) { setModel(model); } else { onDone(error || `Model '${model}' not found`, { display: 'system', }); } } catch (error) { onDone(`Failed to validate model: ${(error as Error).message}`, { display: 'system', }); } } function setModel(modelValue: string | null): void { setAppState(prev => ({ ...prev, mainLoopModel: modelValue, mainLoopModelForSession: null, })); let message = `Set model to ${chalk.bold(renderModelLabel(modelValue))}`; let wasFastModeToggledOn; if (isFastModeEnabled()) { clearFastModeCooldown(); if (!isFastModeSupportedByModel(modelValue) && isFastMode) { setAppState(prev => ({ ...prev, fastMode: false, })); wasFastModeToggledOn = false; // Do not update fast mode in settings since this is an automatic downgrade } else if (isFastModeSupportedByModel(modelValue) && isFastMode) { message += ` · Fast mode ON`; wasFastModeToggledOn = true; } } if (isBilledAsExtraUsage(modelValue, wasFastModeToggledOn === true, isOpus1mMergeEnabled())) { message += ` · Billed as extra usage`; } if (wasFastModeToggledOn === false) { // Fast mode was toggled off, show suffix after extra usage billing message += ` · Fast mode OFF`; } onDone(message); } void handleModelChange(); }, [model, onDone, setAppState]); return null; } function isKnownAlias(model: string): boolean { return (MODEL_ALIASES as readonly string[]).includes(model.toLowerCase().trim()); } function isOpus1mUnavailable(model: string): boolean { const m = model.toLowerCase(); return !checkOpus1mAccess() && !isOpus1mMergeEnabled() && m.includes('opus') && m.includes('[1m]'); } function isSonnet1mUnavailable(model: string): boolean { const m = model.toLowerCase(); // Warn about Sonnet and Sonnet 4.6, but not Sonnet 4.5 since that had // a different access criteria. return !checkSonnet1mAccess() && (m.includes('sonnet[1m]') || m.includes('sonnet-4-6[1m]')); } function ShowModelAndClose({ onDone }: { onDone: (result?: string) => void }): React.ReactNode { const mainLoopModel = useAppState(s => s.mainLoopModel); const mainLoopModelForSession = useAppState(s => s.mainLoopModelForSession); const effortValue = useAppState(s => s.effortValue); const displayModel = renderModelLabel(mainLoopModel); const effortInfo = effortValue !== undefined ? ` (effort: ${effortValue})` : ''; if (mainLoopModelForSession) { onDone( `Current model: ${chalk.bold(renderModelLabel(mainLoopModelForSession))} (session override from plan mode)\nBase model: ${displayModel}${effortInfo}`, ); } else { onDone(`Current model: ${displayModel}${effortInfo}`); } return null; } export const call: LocalJSXCommandCall = async (onDone, _context, args) => { args = args?.trim() || ''; if (COMMON_INFO_ARGS.includes(args)) { logEvent('tengu_model_command_inline_help', { args: args as AnalyticsMetadata_I_VERIFIED_THIS_IS_NOT_CODE_OR_FILEPATHS, }); return ; } if (COMMON_HELP_ARGS.includes(args)) { onDone('Run /model to open the model selection menu, or /model [modelName] to set the model.', { display: 'system', }); return; } if (args) { logEvent('tengu_model_command_inline', { args: args as AnalyticsMetadata_I_VERIFIED_THIS_IS_NOT_CODE_OR_FILEPATHS, }); return ; } return ; }; function renderModelLabel(model: string | null): string { const rendered = renderDefaultModelSetting(model ?? getDefaultMainLoopModelSetting()); return model === null ? `${rendered} (default)` : rendered; }