|
|
'use client'; |
|
|
|
|
|
import { useSubscription } from '@/hooks/react-query/subscriptions/use-subscriptions'; |
|
|
import { useState, useEffect, useMemo } from 'react'; |
|
|
import { isLocalMode } from '@/lib/config'; |
|
|
import { useAvailableModels } from '@/hooks/react-query/subscriptions/use-model'; |
|
|
|
|
|
export const STORAGE_KEY_MODEL = 'suna-preferred-model-v2'; |
|
|
export const STORAGE_KEY_CUSTOM_MODELS = 'customModels'; |
|
|
export const DEFAULT_PREMIUM_MODEL_ID = 'claude-sonnet-4'; |
|
|
|
|
|
export const DEFAULT_FREE_MODEL_ID = 'claude-sonnet-4'; |
|
|
|
|
|
export type SubscriptionStatus = 'no_subscription' | 'active'; |
|
|
|
|
|
export interface ModelOption { |
|
|
id: string; |
|
|
label: string; |
|
|
requiresSubscription: boolean; |
|
|
description?: string; |
|
|
top?: boolean; |
|
|
isCustom?: boolean; |
|
|
priority?: number; |
|
|
} |
|
|
|
|
|
export interface CustomModel { |
|
|
id: string; |
|
|
label: string; |
|
|
} |
|
|
|
|
|
|
|
|
export const MODELS = { |
|
|
|
|
|
'claude-sonnet-4': { |
|
|
tier: 'free', |
|
|
priority: 100, |
|
|
recommended: true, |
|
|
lowQuality: false |
|
|
}, |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
'moonshotai/kimi-k2': { |
|
|
tier: 'free', |
|
|
priority: 99, |
|
|
recommended: false, |
|
|
lowQuality: false |
|
|
}, |
|
|
'grok-4': { |
|
|
tier: 'free', |
|
|
priority: 98, |
|
|
recommended: false, |
|
|
lowQuality: false |
|
|
}, |
|
|
'sonnet-3.7': { |
|
|
tier: 'premium', |
|
|
priority: 97, |
|
|
recommended: false, |
|
|
lowQuality: false |
|
|
}, |
|
|
'google/gemini-2.5-pro': { |
|
|
tier: 'premium', |
|
|
priority: 96, |
|
|
recommended: false, |
|
|
lowQuality: false |
|
|
}, |
|
|
'gpt-4.1': { |
|
|
tier: 'premium', |
|
|
priority: 96, |
|
|
recommended: false, |
|
|
lowQuality: false |
|
|
}, |
|
|
'sonnet-3.5': { |
|
|
tier: 'premium', |
|
|
priority: 90, |
|
|
recommended: false, |
|
|
lowQuality: false |
|
|
}, |
|
|
'gpt-4o': { |
|
|
tier: 'premium', |
|
|
priority: 88, |
|
|
recommended: false, |
|
|
lowQuality: false |
|
|
}, |
|
|
'gemini-2.5-flash:thinking': { |
|
|
tier: 'premium', |
|
|
priority: 84, |
|
|
recommended: false, |
|
|
lowQuality: false |
|
|
}, |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
}; |
|
|
|
|
|
|
|
|
export const canAccessModel = ( |
|
|
subscriptionStatus: SubscriptionStatus, |
|
|
requiresSubscription: boolean, |
|
|
): boolean => { |
|
|
if (isLocalMode()) { |
|
|
return true; |
|
|
} |
|
|
return subscriptionStatus === 'active' || !requiresSubscription; |
|
|
}; |
|
|
|
|
|
|
|
|
export const formatModelName = (name: string): string => { |
|
|
return name |
|
|
.split('-') |
|
|
.map(word => word.charAt(0).toUpperCase() + word.slice(1)) |
|
|
.join(' '); |
|
|
}; |
|
|
|
|
|
|
|
|
export const getPrefixedModelId = (modelId: string, isCustom: boolean): string => { |
|
|
if (isCustom && !modelId.startsWith('openrouter/')) { |
|
|
return `openrouter/${modelId}`; |
|
|
} |
|
|
return modelId; |
|
|
}; |
|
|
|
|
|
|
|
|
export const getCustomModels = (): CustomModel[] => { |
|
|
if (!isLocalMode() || typeof window === 'undefined') return []; |
|
|
|
|
|
try { |
|
|
const storedModels = localStorage.getItem(STORAGE_KEY_CUSTOM_MODELS); |
|
|
if (!storedModels) return []; |
|
|
|
|
|
const parsedModels = JSON.parse(storedModels); |
|
|
if (!Array.isArray(parsedModels)) return []; |
|
|
|
|
|
return parsedModels |
|
|
.filter((model: any) => |
|
|
model && typeof model === 'object' && |
|
|
typeof model.id === 'string' && |
|
|
typeof model.label === 'string'); |
|
|
} catch (e) { |
|
|
console.error('Error parsing custom models:', e); |
|
|
return []; |
|
|
} |
|
|
}; |
|
|
|
|
|
|
|
|
const saveModelPreference = (modelId: string): void => { |
|
|
try { |
|
|
localStorage.setItem(STORAGE_KEY_MODEL, modelId); |
|
|
} catch (error) { |
|
|
console.warn('Failed to save model preference to localStorage:', error); |
|
|
} |
|
|
}; |
|
|
|
|
|
export const useModelSelection = () => { |
|
|
const [selectedModel, setSelectedModel] = useState(DEFAULT_FREE_MODEL_ID); |
|
|
const [customModels, setCustomModels] = useState<CustomModel[]>([]); |
|
|
const [hasInitialized, setHasInitialized] = useState(false); |
|
|
|
|
|
const { data: subscriptionData } = useSubscription(); |
|
|
const { data: modelsData, isLoading: isLoadingModels } = useAvailableModels({ |
|
|
refetchOnMount: false, |
|
|
}); |
|
|
|
|
|
const subscriptionStatus: SubscriptionStatus = subscriptionData?.status === 'active' |
|
|
? 'active' |
|
|
: 'no_subscription'; |
|
|
|
|
|
|
|
|
const refreshCustomModels = () => { |
|
|
if (isLocalMode() && typeof window !== 'undefined') { |
|
|
const freshCustomModels = getCustomModels(); |
|
|
setCustomModels(freshCustomModels); |
|
|
} |
|
|
}; |
|
|
|
|
|
|
|
|
useEffect(() => { |
|
|
refreshCustomModels(); |
|
|
}, []); |
|
|
|
|
|
|
|
|
const MODEL_OPTIONS = useMemo(() => { |
|
|
let models = []; |
|
|
|
|
|
|
|
|
if (!modelsData?.models || isLoadingModels) { |
|
|
models = [ |
|
|
{ |
|
|
id: DEFAULT_FREE_MODEL_ID, |
|
|
label: 'DeepSeek', |
|
|
requiresSubscription: false, |
|
|
priority: MODELS[DEFAULT_FREE_MODEL_ID]?.priority || 50 |
|
|
}, |
|
|
{ |
|
|
id: DEFAULT_PREMIUM_MODEL_ID, |
|
|
label: 'Sonnet 4', |
|
|
requiresSubscription: true, |
|
|
priority: MODELS[DEFAULT_PREMIUM_MODEL_ID]?.priority || 100 |
|
|
}, |
|
|
]; |
|
|
} else { |
|
|
|
|
|
models = modelsData.models.map(model => { |
|
|
const shortName = model.short_name || model.id; |
|
|
const displayName = model.display_name || shortName; |
|
|
|
|
|
|
|
|
let cleanLabel = displayName; |
|
|
if (cleanLabel.includes('/')) { |
|
|
cleanLabel = cleanLabel.split('/').pop() || cleanLabel; |
|
|
} |
|
|
|
|
|
cleanLabel = cleanLabel |
|
|
.replace(/-/g, ' ') |
|
|
.split(' ') |
|
|
.map(word => word.charAt(0).toUpperCase() + word.slice(1)) |
|
|
.join(' '); |
|
|
|
|
|
|
|
|
const modelData = MODELS[shortName] || {}; |
|
|
const isPremium = model?.requires_subscription || modelData.tier === 'premium' || false; |
|
|
|
|
|
return { |
|
|
id: shortName, |
|
|
label: cleanLabel, |
|
|
requiresSubscription: isPremium, |
|
|
top: modelData.priority >= 90, |
|
|
priority: modelData.priority || 0, |
|
|
lowQuality: modelData.lowQuality || false, |
|
|
recommended: modelData.recommended || false |
|
|
}; |
|
|
}); |
|
|
} |
|
|
|
|
|
|
|
|
if (isLocalMode() && customModels.length > 0) { |
|
|
const customModelOptions = customModels.map(model => ({ |
|
|
id: model.id, |
|
|
label: model.label || formatModelName(model.id), |
|
|
requiresSubscription: false, |
|
|
top: false, |
|
|
isCustom: true, |
|
|
priority: 30, |
|
|
lowQuality: false, |
|
|
recommended: false |
|
|
})); |
|
|
|
|
|
models = [...models, ...customModelOptions]; |
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
const sortedModels = models.sort((a, b) => { |
|
|
|
|
|
if (a.recommended !== b.recommended) { |
|
|
return a.recommended ? -1 : 1; |
|
|
} |
|
|
|
|
|
|
|
|
if (a.priority !== b.priority) { |
|
|
return b.priority - a.priority; |
|
|
} |
|
|
|
|
|
|
|
|
return a.label.localeCompare(b.label); |
|
|
}); |
|
|
return sortedModels; |
|
|
}, [modelsData, isLoadingModels, customModels]); |
|
|
|
|
|
|
|
|
const availableModels = useMemo(() => { |
|
|
return isLocalMode() |
|
|
? MODEL_OPTIONS |
|
|
: MODEL_OPTIONS.filter(model => |
|
|
canAccessModel(subscriptionStatus, model.requiresSubscription) |
|
|
); |
|
|
}, [MODEL_OPTIONS, subscriptionStatus]); |
|
|
|
|
|
|
|
|
useEffect(() => { |
|
|
if (typeof window === 'undefined' || hasInitialized) return; |
|
|
|
|
|
console.log('Initializing model selection from localStorage...'); |
|
|
|
|
|
try { |
|
|
const savedModel = localStorage.getItem(STORAGE_KEY_MODEL); |
|
|
console.log('Saved model from localStorage:', savedModel); |
|
|
|
|
|
|
|
|
if (savedModel) { |
|
|
|
|
|
if (isLoadingModels) { |
|
|
console.log('Models still loading, waiting...'); |
|
|
return; |
|
|
} |
|
|
|
|
|
const modelOption = MODEL_OPTIONS.find(option => option.id === savedModel); |
|
|
const isCustomModel = isLocalMode() && customModels.some(model => model.id === savedModel); |
|
|
|
|
|
|
|
|
if (modelOption || isCustomModel) { |
|
|
const isAccessible = isLocalMode() || |
|
|
canAccessModel(subscriptionStatus, modelOption?.requiresSubscription ?? false); |
|
|
|
|
|
if (isAccessible) { |
|
|
console.log('Using saved model:', savedModel); |
|
|
setSelectedModel(savedModel); |
|
|
setHasInitialized(true); |
|
|
return; |
|
|
} else { |
|
|
console.log('Saved model not accessible, falling back to default'); |
|
|
} |
|
|
} else { |
|
|
console.log('Saved model not found in available models, falling back to default'); |
|
|
} |
|
|
} |
|
|
|
|
|
|
|
|
const defaultModel = subscriptionStatus === 'active' ? DEFAULT_PREMIUM_MODEL_ID : DEFAULT_FREE_MODEL_ID; |
|
|
console.log('Using default model:', defaultModel); |
|
|
setSelectedModel(defaultModel); |
|
|
saveModelPreference(defaultModel); |
|
|
setHasInitialized(true); |
|
|
|
|
|
} catch (error) { |
|
|
console.warn('Failed to load preferences from localStorage:', error); |
|
|
const defaultModel = subscriptionStatus === 'active' ? DEFAULT_PREMIUM_MODEL_ID : DEFAULT_FREE_MODEL_ID; |
|
|
setSelectedModel(defaultModel); |
|
|
saveModelPreference(defaultModel); |
|
|
setHasInitialized(true); |
|
|
} |
|
|
}, [subscriptionStatus, MODEL_OPTIONS, isLoadingModels, customModels, hasInitialized]); |
|
|
|
|
|
|
|
|
const handleModelChange = (modelId: string) => { |
|
|
console.log('handleModelChange called with:', modelId); |
|
|
|
|
|
|
|
|
if (isLocalMode()) { |
|
|
refreshCustomModels(); |
|
|
} |
|
|
|
|
|
|
|
|
const isCustomModel = isLocalMode() && customModels.some(model => model.id === modelId); |
|
|
|
|
|
|
|
|
const modelOption = MODEL_OPTIONS.find(option => option.id === modelId); |
|
|
|
|
|
|
|
|
if (!modelOption && !isCustomModel) { |
|
|
console.warn('Model not found in options:', modelId, MODEL_OPTIONS, isCustomModel, customModels); |
|
|
|
|
|
|
|
|
const defaultModel = isLocalMode() ? DEFAULT_PREMIUM_MODEL_ID : DEFAULT_FREE_MODEL_ID; |
|
|
setSelectedModel(defaultModel); |
|
|
saveModelPreference(defaultModel); |
|
|
return; |
|
|
} |
|
|
|
|
|
|
|
|
if (!isCustomModel && !isLocalMode() && |
|
|
!canAccessModel(subscriptionStatus, modelOption?.requiresSubscription ?? false)) { |
|
|
console.warn('Model not accessible:', modelId); |
|
|
return; |
|
|
} |
|
|
|
|
|
console.log('Setting selected model and saving to localStorage:', modelId); |
|
|
setSelectedModel(modelId); |
|
|
saveModelPreference(modelId); |
|
|
}; |
|
|
|
|
|
|
|
|
const getActualModelId = (modelId: string): string => { |
|
|
|
|
|
return modelId; |
|
|
}; |
|
|
|
|
|
return { |
|
|
selectedModel, |
|
|
setSelectedModel: (modelId: string) => { |
|
|
handleModelChange(modelId); |
|
|
}, |
|
|
subscriptionStatus, |
|
|
availableModels, |
|
|
allModels: MODEL_OPTIONS, |
|
|
customModels, |
|
|
getActualModelId, |
|
|
refreshCustomModels, |
|
|
canAccessModel: (modelId: string) => { |
|
|
if (isLocalMode()) return true; |
|
|
const model = MODEL_OPTIONS.find(m => m.id === modelId); |
|
|
return model ? canAccessModel(subscriptionStatus, model.requiresSubscription) : false; |
|
|
}, |
|
|
isSubscriptionRequired: (modelId: string) => { |
|
|
return MODEL_OPTIONS.find(m => m.id === modelId)?.requiresSubscription || false; |
|
|
} |
|
|
}; |
|
|
}; |
|
|
|
|
|
|