File size: 4,633 Bytes
064bfd6 | 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 | // biome-ignore-all assist/source/organizeImports: ANT-ONLY import markers must not be reordered
import { MODEL_ALIASES } from './aliases.js'
import { isModelAllowed } from './modelAllowlist.js'
import { getAPIProvider } from './providers.js'
import { sideQuery } from '../sideQuery.js'
import {
NotFoundError,
APIError,
APIConnectionError,
AuthenticationError,
} from '@anthropic-ai/sdk'
import { getModelStrings } from './modelStrings.js'
// Cache valid models to avoid repeated API calls
const validModelCache = new Map<string, boolean>()
/**
* Validates a model by attempting an actual API call.
*/
export async function validateModel(
model: string,
): Promise<{ valid: boolean; error?: string }> {
const normalizedModel = model.trim()
// Empty model is invalid
if (!normalizedModel) {
return { valid: false, error: 'Model name cannot be empty' }
}
// Check against availableModels allowlist before any API call
if (!isModelAllowed(normalizedModel)) {
return {
valid: false,
error: `Model '${normalizedModel}' is not in the list of available models`,
}
}
// Check if it's a known alias (these are always valid)
const lowerModel = normalizedModel.toLowerCase()
if ((MODEL_ALIASES as readonly string[]).includes(lowerModel)) {
return { valid: true }
}
// Check if it matches ANTHROPIC_CUSTOM_MODEL_OPTION (pre-validated by the user)
if (normalizedModel === process.env.ANTHROPIC_CUSTOM_MODEL_OPTION) {
return { valid: true }
}
// Check cache first
if (validModelCache.has(normalizedModel)) {
return { valid: true }
}
// Try to make an actual API call with minimal parameters
try {
await sideQuery({
model: normalizedModel,
max_tokens: 1,
maxRetries: 0,
querySource: 'model_validation',
messages: [
{
role: 'user',
content: [
{
type: 'text',
text: 'Hi',
cache_control: { type: 'ephemeral' },
},
],
},
],
})
// If we got here, the model is valid
validModelCache.set(normalizedModel, true)
return { valid: true }
} catch (error) {
return handleValidationError(error, normalizedModel)
}
}
function handleValidationError(
error: unknown,
modelName: string,
): { valid: boolean; error: string } {
// NotFoundError (404) means the model doesn't exist
if (error instanceof NotFoundError) {
const fallback = get3PFallbackSuggestion(modelName)
const suggestion = fallback ? `. Try '${fallback}' instead` : ''
return {
valid: false,
error: `Model '${modelName}' not found${suggestion}`,
}
}
// For other API errors, provide context-specific messages
if (error instanceof APIError) {
if (error instanceof AuthenticationError) {
return {
valid: false,
error: 'Authentication failed. Please check your API credentials.',
}
}
if (error instanceof APIConnectionError) {
return {
valid: false,
error: 'Network error. Please check your internet connection.',
}
}
// Check error body for model-specific errors
const errorBody = error.error as unknown
if (
errorBody &&
typeof errorBody === 'object' &&
'type' in errorBody &&
errorBody.type === 'not_found_error' &&
'message' in errorBody &&
typeof errorBody.message === 'string' &&
errorBody.message.includes('model:')
) {
return { valid: false, error: `Model '${modelName}' not found` }
}
// Generic API error
return { valid: false, error: `API error: ${error.message}` }
}
// For unknown errors, be safe and reject
const errorMessage = error instanceof Error ? error.message : String(error)
return {
valid: false,
error: `Unable to validate model: ${errorMessage}`,
}
}
// @[MODEL LAUNCH]: Add a fallback suggestion chain for the new model → previous version
/**
* Suggest a fallback model for 3P users when the selected model is unavailable.
*/
function get3PFallbackSuggestion(model: string): string | undefined {
if (getAPIProvider() === 'firstParty') {
return undefined
}
const lowerModel = model.toLowerCase()
if (lowerModel.includes('opus-4-6') || lowerModel.includes('opus_4_6')) {
return getModelStrings().opus41
}
if (lowerModel.includes('sonnet-4-6') || lowerModel.includes('sonnet_4_6')) {
return getModelStrings().sonnet45
}
if (lowerModel.includes('sonnet-4-5') || lowerModel.includes('sonnet_4_5')) {
return getModelStrings().sonnet40
}
return undefined
}
|