/ utils / model / validateModel.ts
validateModel.ts
  1  // biome-ignore-all assist/source/organizeImports: ANT-ONLY import markers must not be reordered
  2  import { MODEL_ALIASES } from './aliases.js'
  3  import { isModelAllowed } from './modelAllowlist.js'
  4  import { getAPIProvider } from './providers.js'
  5  import { sideQuery } from '../sideQuery.js'
  6  import {
  7    NotFoundError,
  8    APIError,
  9    APIConnectionError,
 10    AuthenticationError,
 11  } from '@anthropic-ai/sdk'
 12  import { getModelStrings } from './modelStrings.js'
 13  
 14  // Cache valid models to avoid repeated API calls
 15  const validModelCache = new Map<string, boolean>()
 16  
 17  /**
 18   * Validates a model by attempting an actual API call.
 19   */
 20  export async function validateModel(
 21    model: string,
 22  ): Promise<{ valid: boolean; error?: string }> {
 23    const normalizedModel = model.trim()
 24  
 25    // Empty model is invalid
 26    if (!normalizedModel) {
 27      return { valid: false, error: 'Model name cannot be empty' }
 28    }
 29  
 30    // Check against availableModels allowlist before any API call
 31    if (!isModelAllowed(normalizedModel)) {
 32      return {
 33        valid: false,
 34        error: `Model '${normalizedModel}' is not in the list of available models`,
 35      }
 36    }
 37  
 38    // Check if it's a known alias (these are always valid)
 39    const lowerModel = normalizedModel.toLowerCase()
 40    if ((MODEL_ALIASES as readonly string[]).includes(lowerModel)) {
 41      return { valid: true }
 42    }
 43  
 44    // Check if it matches ANTHROPIC_CUSTOM_MODEL_OPTION (pre-validated by the user)
 45    if (normalizedModel === process.env.ANTHROPIC_CUSTOM_MODEL_OPTION) {
 46      return { valid: true }
 47    }
 48  
 49    // Check cache first
 50    if (validModelCache.has(normalizedModel)) {
 51      return { valid: true }
 52    }
 53  
 54  
 55    // Try to make an actual API call with minimal parameters
 56    try {
 57      await sideQuery({
 58        model: normalizedModel,
 59        max_tokens: 1,
 60        maxRetries: 0,
 61        querySource: 'model_validation',
 62        messages: [
 63          {
 64            role: 'user',
 65            content: [
 66              {
 67                type: 'text',
 68                text: 'Hi',
 69                cache_control: { type: 'ephemeral' },
 70              },
 71            ],
 72          },
 73        ],
 74      })
 75  
 76      // If we got here, the model is valid
 77      validModelCache.set(normalizedModel, true)
 78      return { valid: true }
 79    } catch (error) {
 80      return handleValidationError(error, normalizedModel)
 81    }
 82  }
 83  
 84  function handleValidationError(
 85    error: unknown,
 86    modelName: string,
 87  ): { valid: boolean; error: string } {
 88    // NotFoundError (404) means the model doesn't exist
 89    if (error instanceof NotFoundError) {
 90      const fallback = get3PFallbackSuggestion(modelName)
 91      const suggestion = fallback ? `. Try '${fallback}' instead` : ''
 92      return {
 93        valid: false,
 94        error: `Model '${modelName}' not found${suggestion}`,
 95      }
 96    }
 97  
 98    // For other API errors, provide context-specific messages
 99    if (error instanceof APIError) {
100      if (error instanceof AuthenticationError) {
101        return {
102          valid: false,
103          error: 'Authentication failed. Please check your API credentials.',
104        }
105      }
106  
107      if (error instanceof APIConnectionError) {
108        return {
109          valid: false,
110          error: 'Network error. Please check your internet connection.',
111        }
112      }
113  
114      // Check error body for model-specific errors
115      const errorBody = error.error as unknown
116      if (
117        errorBody &&
118        typeof errorBody === 'object' &&
119        'type' in errorBody &&
120        errorBody.type === 'not_found_error' &&
121        'message' in errorBody &&
122        typeof errorBody.message === 'string' &&
123        errorBody.message.includes('model:')
124      ) {
125        return { valid: false, error: `Model '${modelName}' not found` }
126      }
127  
128      // Generic API error
129      return { valid: false, error: `API error: ${error.message}` }
130    }
131  
132    // For unknown errors, be safe and reject
133    const errorMessage = error instanceof Error ? error.message : String(error)
134    return {
135      valid: false,
136      error: `Unable to validate model: ${errorMessage}`,
137    }
138  }
139  
140  // @[MODEL LAUNCH]: Add a fallback suggestion chain for the new model → previous version
141  /**
142   * Suggest a fallback model for 3P users when the selected model is unavailable.
143   */
144  function get3PFallbackSuggestion(model: string): string | undefined {
145    if (getAPIProvider() === 'firstParty') {
146      return undefined
147    }
148    const lowerModel = model.toLowerCase()
149    if (lowerModel.includes('opus-4-6') || lowerModel.includes('opus_4_6')) {
150      return getModelStrings().opus41
151    }
152    if (lowerModel.includes('sonnet-4-6') || lowerModel.includes('sonnet_4_6')) {
153      return getModelStrings().sonnet45
154    }
155    if (lowerModel.includes('sonnet-4-5') || lowerModel.includes('sonnet_4_5')) {
156      return getModelStrings().sonnet40
157    }
158    return undefined
159  }