File size: 19,219 Bytes
4efde5d
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
'use client';

import { useSubscriptionData } from '@/contexts/SubscriptionContext';
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-v3';
export const STORAGE_KEY_CUSTOM_MODELS = 'customModels';
export const DEFAULT_PREMIUM_MODEL_ID = 'claude-sonnet-4';
export const DEFAULT_FREE_MODEL_ID = 'moonshotai/kimi-k2';

export const testLocalStorage = (): boolean => {
  if (typeof window === 'undefined') return false;
  try {
    const testKey = 'test-storage';
    const testValue = 'test-value';
    localStorage.setItem(testKey, testValue);
    const retrieved = localStorage.getItem(testKey);
    localStorage.removeItem(testKey);
    return retrieved === testValue;
  } catch (error) {
    console.error('localStorage test failed:', error);
    return false;
  }
};

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: 'premium',
    priority: 100, 
    recommended: true,
    lowQuality: false
  },
  'gpt-5': { 
    tier: 'premium', 
    priority: 99,
    recommended: false,
    lowQuality: false
  },
  'google/gemini-2.5-pro': { 
    tier: 'premium', 
    priority: 96,
    recommended: false,
    lowQuality: false
  },
  'grok-4': { 
    tier: 'premium', 
    priority: 94,
    recommended: false,
    lowQuality: false
  },
  'sonnet-3.7': { 
    tier: 'premium', 
    priority: 93, 
    recommended: false,
    lowQuality: false
  },
  'sonnet-3.5': { 
    tier: 'premium', 
    priority: 90,
    recommended: false,
    lowQuality: false
  },

  'moonshotai/kimi-k2': { 
    tier: 'free', 
    priority: 100,
    recommended: true,
    lowQuality: false
  },
  'deepseek': { 
    tier: 'free', 
    priority: 95,
    recommended: false,
    lowQuality: false
  },
  'qwen3': { 
    tier: 'free', 
    priority: 90,
    recommended: false,
    lowQuality: false
  },
  'gpt-5-mini': { 
    tier: 'free', 
    priority: 85,
    recommended: false,
    lowQuality: false
  },
};

// Helper to check if a user can access a model based on subscription status
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;
};

// Helper to get custom models from localStorage
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 [];
  }
};

// Helper to save model preference to localStorage safely
const saveModelPreference = (modelId: string): void => {
  try {
    localStorage.setItem(STORAGE_KEY_MODEL, modelId);
    console.log('βœ… useModelSelection: Saved model preference to localStorage:', modelId);
  } catch (error) {
    console.warn('❌ useModelSelection: Failed to save model preference to localStorage:', error);
  }
};

export const useModelSelectionOld = () => {
  const [selectedModel, setSelectedModel] = useState(DEFAULT_FREE_MODEL_ID);
  const [customModels, setCustomModels] = useState<CustomModel[]>([]);
  const [hasInitialized, setHasInitialized] = useState(false);
  
  const { data: subscriptionData } = useSubscriptionData();
  const { data: modelsData, isLoading: isLoadingModels } = useAvailableModels({
    refetchOnMount: false,
  });
  
  const subscriptionStatus: SubscriptionStatus = (subscriptionData?.status === 'active' || subscriptionData?.status === 'trialing')
    ? 'active' 
    : 'no_subscription';

  // Function to refresh custom models from localStorage
  const refreshCustomModels = () => {
    if (isLocalMode() && typeof window !== 'undefined') {
      const freshCustomModels = getCustomModels();
      setCustomModels(freshCustomModels);
    }
  };

  // Load custom models from localStorage
  useEffect(() => {
    refreshCustomModels();
  }, []);

  // Generate model options list with consistent structure
  const MODEL_OPTIONS = useMemo(() => {
    let models = [];
    
    // Default models if API data not available
    if (!modelsData?.models || isLoadingModels) {
      models = [
        { 
          id: DEFAULT_FREE_MODEL_ID, 
          label: 'KIMI K2', 
          requiresSubscription: false,
          priority: 100,
          recommended: true
        },
        { 
          id: DEFAULT_PREMIUM_MODEL_ID, 
          label: 'Claude Sonnet 4', 
          requiresSubscription: true, 
          priority: 100,
          recommended: true
        },
      ];
    } else {
      // Process API-provided models - use clean data from new backend system
      models = modelsData.models.map(model => {
        // Use the clean data directly from the API (no more duplicates!)
        const shortName = model.short_name || model.id;
        const displayName = model.display_name || shortName;
        
        return {
          id: shortName,
          label: displayName,
          requiresSubscription: model.requires_subscription || false,
          priority: model.priority || 0,
          recommended: model.recommended || false,
          top: (model.priority || 0) >= 90, // Mark high-priority models as "top"
          lowQuality: false, // All models in new system are quality controlled
          capabilities: model.capabilities || [],
          contextWindow: model.context_window || 128000
        };
      });
    }
    
    // Add custom models if in local mode
    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, // Low priority by default
        lowQuality: false,
        recommended: false
      }));
      
      models = [...models, ...customModelOptions];
    }
    
    // Sort models consistently in one place:
    // 1. First by recommended (recommended first)
    // 2. Then by priority (higher first)
    // 3. Finally by name (alphabetical)
    const sortedModels = models.sort((a, b) => {
      // First by recommended status
      if (a.recommended !== b.recommended) {
        return a.recommended ? -1 : 1;
      }

      // Then by priority (higher first)
      if (a.priority !== b.priority) {
        return b.priority - a.priority;
      }
      
      // Finally by name
      return a.label.localeCompare(b.label);
    });
    return sortedModels;
  }, [modelsData, isLoadingModels, customModels]);

  // Get filtered list of models the user can access (no additional sorting)
  const availableModels = useMemo(() => {
    return isLocalMode() 
      ? MODEL_OPTIONS 
      : MODEL_OPTIONS.filter(model => 
          canAccessModel(subscriptionStatus, model.requiresSubscription)
        );
  }, [MODEL_OPTIONS, subscriptionStatus]);

  // Initialize selected model from localStorage ONLY ONCE
  useEffect(() => {
    if (typeof window === 'undefined' || hasInitialized) return;
    
    console.log('πŸ”§ useModelSelection: Initializing model selection...');
    console.log('πŸ”§ useModelSelection: isLoadingModels:', isLoadingModels);
    console.log('πŸ”§ useModelSelection: subscriptionStatus:', subscriptionStatus);
    console.log('πŸ”§ useModelSelection: localStorage test passed:', testLocalStorage());
    
    try {
      const savedModel = localStorage.getItem(STORAGE_KEY_MODEL);
      console.log('πŸ”§ useModelSelection: Saved model from localStorage:', savedModel);
      
      // If we have a saved model, validate it's still available and accessible
      if (savedModel) {
        // Wait for models to load before validating
        if (isLoadingModels) {
          console.log('πŸ”§ useModelSelection: Models still loading, using saved model temporarily:', savedModel);
          // Use saved model immediately while waiting for validation
          setSelectedModel(savedModel);
          setHasInitialized(true);
          return;
        }
        
        console.log('πŸ”§ useModelSelection: Available MODEL_OPTIONS:', MODEL_OPTIONS.map(m => ({ id: m.id, requiresSubscription: m.requiresSubscription })));
        
        const modelOption = MODEL_OPTIONS.find(option => option.id === savedModel);
        const isCustomModel = isLocalMode() && customModels.some(model => model.id === savedModel);
        
        console.log('πŸ”§ useModelSelection: modelOption found:', modelOption);
        console.log('πŸ”§ useModelSelection: isCustomModel:', isCustomModel);
        
        // Check if saved model is still valid and accessible
        if (modelOption || isCustomModel) {
          const isAccessible = isLocalMode() || 
            canAccessModel(subscriptionStatus, modelOption?.requiresSubscription ?? false);
          
          console.log('πŸ”§ useModelSelection: isAccessible:', isAccessible);
          
          if (isAccessible) {
            console.log('βœ… useModelSelection: Using saved model:', savedModel);
            setSelectedModel(savedModel);
            setHasInitialized(true);
            return;
          } else {
            console.warn('⚠️ useModelSelection: Saved model not accessible with current subscription');
          }
        } else {
          // Model not found in current options, but preserve it anyway in case it's valid
          // This can happen during loading or if the API returns different models
          console.warn('⚠️ useModelSelection: Saved model not found in available options, but preserving:', savedModel);
          setSelectedModel(savedModel);
          setHasInitialized(true);
          return;
        }
      }
      
      // Fallback to default model
      const defaultModel = subscriptionStatus === 'active' ? DEFAULT_PREMIUM_MODEL_ID : DEFAULT_FREE_MODEL_ID;
      console.log('πŸ”§ useModelSelection: Using default model:', defaultModel);
      console.log('πŸ”§ useModelSelection: Subscription status:', subscriptionStatus, '-> Default:', subscriptionStatus === 'active' ? 'PREMIUM (Claude Sonnet 4)' : 'FREE (KIMi K2)');
      setSelectedModel(defaultModel);
      saveModelPreference(defaultModel);
      setHasInitialized(true);
      
    } catch (error) {
      console.warn('❌ useModelSelection: Failed to load preferences from localStorage:', error);
      const defaultModel = subscriptionStatus === 'active' ? DEFAULT_PREMIUM_MODEL_ID : DEFAULT_FREE_MODEL_ID;
      console.log('πŸ”§ useModelSelection: Using fallback default model:', defaultModel);
      console.log('πŸ”§ useModelSelection: Subscription status:', subscriptionStatus, '-> Fallback:', subscriptionStatus === 'active' ? 'PREMIUM (Claude Sonnet 4)' : 'FREE (KIMi K2)');
      setSelectedModel(defaultModel);
      saveModelPreference(defaultModel);
      setHasInitialized(true);
    }
  }, [subscriptionStatus, isLoadingModels, hasInitialized]);

  // Re-validate saved model after loading completes
  useEffect(() => {
    if (!hasInitialized || typeof window === 'undefined' || isLoadingModels) return;
    
    const savedModel = localStorage.getItem(STORAGE_KEY_MODEL);
    if (!savedModel || savedModel === selectedModel) return;
    
    console.log('πŸ”§ useModelSelection: Re-validating saved model after loading:', savedModel);
    
    const modelOption = MODEL_OPTIONS.find(option => option.id === savedModel);
    const isCustomModel = isLocalMode() && customModels.some(model => model.id === savedModel);
    
    // If the saved model is now invalid, switch to default
    if (!modelOption && !isCustomModel) {
      console.warn('⚠️ useModelSelection: Saved model is invalid after loading, switching to default');
      const defaultModel = subscriptionStatus === 'active' ? DEFAULT_PREMIUM_MODEL_ID : DEFAULT_FREE_MODEL_ID;
      setSelectedModel(defaultModel);
      saveModelPreference(defaultModel);
    } else if (modelOption && !isLocalMode()) {
      // Check subscription access for non-custom models
      const isAccessible = canAccessModel(subscriptionStatus, modelOption.requiresSubscription);
      if (!isAccessible) {
        console.warn('⚠️ useModelSelection: Saved model not accessible after subscription check, switching to default');
        const defaultModel = subscriptionStatus === 'active' ? DEFAULT_PREMIUM_MODEL_ID : DEFAULT_FREE_MODEL_ID;
        setSelectedModel(defaultModel);
        saveModelPreference(defaultModel);
      }
    }
  }, [isLoadingModels, hasInitialized, MODEL_OPTIONS, customModels, subscriptionStatus]);

  // Re-validate current model when subscription status changes
  useEffect(() => {
    if (!hasInitialized || typeof window === 'undefined') return;
    
    console.log('πŸ”§ useModelSelection: Subscription status changed, re-validating current model...');
    console.log('πŸ”§ useModelSelection: Current selected model:', selectedModel);
    console.log('πŸ”§ useModelSelection: New subscription status:', subscriptionStatus);
    
    // Skip validation if models are still loading
    if (isLoadingModels) return;
    
    // Check if current model is still accessible
    const modelOption = MODEL_OPTIONS.find(option => option.id === selectedModel);
    const isCustomModel = isLocalMode() && customModels.some(model => model.id === selectedModel);
    
    if (modelOption && !isCustomModel && !isLocalMode()) {
      const isAccessible = canAccessModel(subscriptionStatus, modelOption.requiresSubscription);
      
      if (!isAccessible) {
        console.warn('⚠️ useModelSelection: Current model no longer accessible, switching to default');
        const defaultModel = subscriptionStatus === 'active' ? DEFAULT_PREMIUM_MODEL_ID : DEFAULT_FREE_MODEL_ID;
        console.log('πŸ”§ useModelSelection: Subscription-based default switch:', subscriptionStatus === 'active' ? 'PREMIUM (Claude Sonnet 4)' : 'FREE (KIMi K2)');
        setSelectedModel(defaultModel);
        saveModelPreference(defaultModel);
      } else {
        console.log('βœ… useModelSelection: Current model still accessible');
      }
    }
  }, [subscriptionStatus, selectedModel, hasInitialized, isLoadingModels]);

  // Handle model selection change
  const handleModelChange = (modelId: string) => {
    console.log('πŸ”§ useModelSelection: handleModelChange called with:', modelId);
    console.log('πŸ”§ useModelSelection: Available MODEL_OPTIONS:', MODEL_OPTIONS.map(m => m.id));
    
    // Refresh custom models from localStorage to ensure we have the latest
    if (isLocalMode()) {
      refreshCustomModels();
    }
    
    // First check if it's a custom model in local mode
    const isCustomModel = isLocalMode() && customModels.some(model => model.id === modelId);
    
    // Then check if it's in standard MODEL_OPTIONS
    const modelOption = MODEL_OPTIONS.find(option => option.id === modelId);
    
    console.log('πŸ”§ useModelSelection: modelOption found:', modelOption);
    console.log('πŸ”§ useModelSelection: isCustomModel:', isCustomModel);
    
    // Check if model exists in either custom models or standard options
    if (!modelOption && !isCustomModel) {
      console.warn('πŸ”§ useModelSelection: Model not found in options:', modelId, MODEL_OPTIONS, isCustomModel, customModels);
      
      // Reset to default model when the selected model is not found
      const defaultModel = isLocalMode() ? DEFAULT_PREMIUM_MODEL_ID : DEFAULT_FREE_MODEL_ID;
      console.log('πŸ”§ useModelSelection: Resetting to default model:', defaultModel);
      setSelectedModel(defaultModel);
      saveModelPreference(defaultModel);
      return;
    }

    // Check access permissions (except for custom models in local mode)
    if (!isCustomModel && !isLocalMode() && 
        !canAccessModel(subscriptionStatus, modelOption?.requiresSubscription ?? false)) {
      console.warn('πŸ”§ useModelSelection: Model not accessible:', modelId);
      return;
    }
    
    console.log('βœ… useModelSelection: Setting model to:', modelId);
    setSelectedModel(modelId);
    saveModelPreference(modelId);
    console.log('βœ… useModelSelection: Model change completed successfully');
  };

  // Get the actual model ID to send to the backend
  const getActualModelId = (modelId: string): string => {
    // No need for automatic prefixing in most cases - just return as is
    return modelId;
  };

  return {
    selectedModel,
    setSelectedModel: (modelId: string) => {
      handleModelChange(modelId);
    },
    subscriptionStatus,
    availableModels,
    allModels: MODEL_OPTIONS,  // Already pre-sorted
    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;
    },
    // Debug utility to check current state
    debugState: () => {
      console.log('πŸ”§ useModelSelection Debug State:');
      console.log('  selectedModel:', selectedModel);
      console.log('  hasInitialized:', hasInitialized);
      console.log('  subscriptionStatus:', subscriptionStatus);
      console.log('  isLoadingModels:', isLoadingModels);
      console.log('  localStorage value:', localStorage.getItem(STORAGE_KEY_MODEL));
      console.log('  localStorage test passes:', testLocalStorage());
      console.log('  defaultModel would be:', subscriptionStatus === 'active' ? `${DEFAULT_PREMIUM_MODEL_ID} (Claude Sonnet 4)` : `${DEFAULT_FREE_MODEL_ID} (KIMi K2)`);
      console.log('  availableModels:', availableModels.map(m => ({ id: m.id, requiresSubscription: m.requiresSubscription })));
    }
  };
};

// Export the new model selection hook
export { useModelSelection } from './_use-model-selection-new';

// Export the hook but not any sorting logic - sorting is handled internally