source dump of claude code
at main 170 lines 6.0 kB view raw
1import { getSettings_DEPRECATED } from '../settings/settings.js' 2import { isModelAlias, isModelFamilyAlias } from './aliases.js' 3import { parseUserSpecifiedModel } from './model.js' 4import { resolveOverriddenModel } from './modelStrings.js' 5 6/** 7 * Check if a model belongs to a given family by checking if its name 8 * (or resolved name) contains the family identifier. 9 */ 10function modelBelongsToFamily(model: string, family: string): boolean { 11 if (model.includes(family)) { 12 return true 13 } 14 // Resolve aliases like "best" → "claude-opus-4-6" to check family membership 15 if (isModelAlias(model)) { 16 const resolved = parseUserSpecifiedModel(model).toLowerCase() 17 return resolved.includes(family) 18 } 19 return false 20} 21 22/** 23 * Check if a model name starts with a prefix at a segment boundary. 24 * The prefix must match up to the end of the name or a "-" separator. 25 * e.g. "claude-opus-4-5" matches "claude-opus-4-5-20251101" but not "claude-opus-4-50". 26 */ 27function prefixMatchesModel(modelName: string, prefix: string): boolean { 28 if (!modelName.startsWith(prefix)) { 29 return false 30 } 31 return modelName.length === prefix.length || modelName[prefix.length] === '-' 32} 33 34/** 35 * Check if a model matches a version-prefix entry in the allowlist. 36 * Supports shorthand like "opus-4-5" (mapped to "claude-opus-4-5") and 37 * full prefixes like "claude-opus-4-5". Resolves input aliases before matching. 38 */ 39function modelMatchesVersionPrefix(model: string, entry: string): boolean { 40 // Resolve the input model to a full name if it's an alias 41 const resolvedModel = isModelAlias(model) 42 ? parseUserSpecifiedModel(model).toLowerCase() 43 : model 44 45 // Try the entry as-is (e.g. "claude-opus-4-5") 46 if (prefixMatchesModel(resolvedModel, entry)) { 47 return true 48 } 49 // Try with "claude-" prefix (e.g. "opus-4-5" → "claude-opus-4-5") 50 if ( 51 !entry.startsWith('claude-') && 52 prefixMatchesModel(resolvedModel, `claude-${entry}`) 53 ) { 54 return true 55 } 56 return false 57} 58 59/** 60 * Check if a family alias is narrowed by more specific entries in the allowlist. 61 * When the allowlist contains both "opus" and "opus-4-5", the specific entry 62 * takes precedence — "opus" alone would be a wildcard, but "opus-4-5" narrows 63 * it to only that version. 64 */ 65function familyHasSpecificEntries( 66 family: string, 67 allowlist: string[], 68): boolean { 69 for (const entry of allowlist) { 70 if (isModelFamilyAlias(entry)) { 71 continue 72 } 73 // Check if entry is a version-qualified variant of this family 74 // e.g., "opus-4-5" or "claude-opus-4-5-20251101" for the "opus" family 75 // Must match at a segment boundary (followed by '-' or end) to avoid 76 // false positives like "opusplan" matching "opus" 77 const idx = entry.indexOf(family) 78 if (idx === -1) { 79 continue 80 } 81 const afterFamily = idx + family.length 82 if (afterFamily === entry.length || entry[afterFamily] === '-') { 83 return true 84 } 85 } 86 return false 87} 88 89/** 90 * Check if a model is allowed by the availableModels allowlist in settings. 91 * If availableModels is not set, all models are allowed. 92 * 93 * Matching tiers: 94 * 1. Family aliases ("opus", "sonnet", "haiku") — wildcard for the entire family, 95 * UNLESS more specific entries for that family also exist (e.g., "opus-4-5"). 96 * In that case, the family wildcard is ignored and only the specific entries apply. 97 * 2. Version prefixes ("opus-4-5", "claude-opus-4-5") — any build of that version 98 * 3. Full model IDs ("claude-opus-4-5-20251101") — exact match only 99 */ 100export function isModelAllowed(model: string): boolean { 101 const settings = getSettings_DEPRECATED() || {} 102 const { availableModels } = settings 103 if (!availableModels) { 104 return true // No restrictions 105 } 106 if (availableModels.length === 0) { 107 return false // Empty allowlist blocks all user-specified models 108 } 109 110 const resolvedModel = resolveOverriddenModel(model) 111 const normalizedModel = resolvedModel.trim().toLowerCase() 112 const normalizedAllowlist = availableModels.map(m => m.trim().toLowerCase()) 113 114 // Direct match (alias-to-alias or full-name-to-full-name) 115 // Skip family aliases that have been narrowed by specific entries — 116 // e.g., "opus" in ["opus", "opus-4-5"] should NOT directly match, 117 // because the admin intends to restrict to opus 4.5 only. 118 if (normalizedAllowlist.includes(normalizedModel)) { 119 if ( 120 !isModelFamilyAlias(normalizedModel) || 121 !familyHasSpecificEntries(normalizedModel, normalizedAllowlist) 122 ) { 123 return true 124 } 125 } 126 127 // Family-level aliases in the allowlist match any model in that family, 128 // but only if no more specific entries exist for that family. 129 // e.g., ["opus"] allows all opus, but ["opus", "opus-4-5"] only allows opus 4.5. 130 for (const entry of normalizedAllowlist) { 131 if ( 132 isModelFamilyAlias(entry) && 133 !familyHasSpecificEntries(entry, normalizedAllowlist) && 134 modelBelongsToFamily(normalizedModel, entry) 135 ) { 136 return true 137 } 138 } 139 140 // For non-family entries, do bidirectional alias resolution 141 // If model is an alias, resolve it and check if the resolved name is in the list 142 if (isModelAlias(normalizedModel)) { 143 const resolved = parseUserSpecifiedModel(normalizedModel).toLowerCase() 144 if (normalizedAllowlist.includes(resolved)) { 145 return true 146 } 147 } 148 149 // If any non-family alias in the allowlist resolves to the input model 150 for (const entry of normalizedAllowlist) { 151 if (!isModelFamilyAlias(entry) && isModelAlias(entry)) { 152 const resolved = parseUserSpecifiedModel(entry).toLowerCase() 153 if (resolved === normalizedModel) { 154 return true 155 } 156 } 157 } 158 159 // Version-prefix matching: "opus-4-5" or "claude-opus-4-5" matches 160 // "claude-opus-4-5-20251101" at a segment boundary 161 for (const entry of normalizedAllowlist) { 162 if (!isModelFamilyAlias(entry) && !isModelAlias(entry)) { 163 if (modelMatchesVersionPrefix(normalizedModel, entry)) { 164 return true 165 } 166 } 167 } 168 169 return false 170}