File size: 6,030 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 161 162 163 164 165 166 167 168 169 170 171 | import { getSettings_DEPRECATED } from '../settings/settings.js'
import { isModelAlias, isModelFamilyAlias } from './aliases.js'
import { parseUserSpecifiedModel } from './model.js'
import { resolveOverriddenModel } from './modelStrings.js'
/**
* Check if a model belongs to a given family by checking if its name
* (or resolved name) contains the family identifier.
*/
function modelBelongsToFamily(model: string, family: string): boolean {
if (model.includes(family)) {
return true
}
// Resolve aliases like "best" → "claude-opus-4-6" to check family membership
if (isModelAlias(model)) {
const resolved = parseUserSpecifiedModel(model).toLowerCase()
return resolved.includes(family)
}
return false
}
/**
* Check if a model name starts with a prefix at a segment boundary.
* The prefix must match up to the end of the name or a "-" separator.
* e.g. "claude-opus-4-5" matches "claude-opus-4-5-20251101" but not "claude-opus-4-50".
*/
function prefixMatchesModel(modelName: string, prefix: string): boolean {
if (!modelName.startsWith(prefix)) {
return false
}
return modelName.length === prefix.length || modelName[prefix.length] === '-'
}
/**
* Check if a model matches a version-prefix entry in the allowlist.
* Supports shorthand like "opus-4-5" (mapped to "claude-opus-4-5") and
* full prefixes like "claude-opus-4-5". Resolves input aliases before matching.
*/
function modelMatchesVersionPrefix(model: string, entry: string): boolean {
// Resolve the input model to a full name if it's an alias
const resolvedModel = isModelAlias(model)
? parseUserSpecifiedModel(model).toLowerCase()
: model
// Try the entry as-is (e.g. "claude-opus-4-5")
if (prefixMatchesModel(resolvedModel, entry)) {
return true
}
// Try with "claude-" prefix (e.g. "opus-4-5" → "claude-opus-4-5")
if (
!entry.startsWith('claude-') &&
prefixMatchesModel(resolvedModel, `claude-${entry}`)
) {
return true
}
return false
}
/**
* Check if a family alias is narrowed by more specific entries in the allowlist.
* When the allowlist contains both "opus" and "opus-4-5", the specific entry
* takes precedence — "opus" alone would be a wildcard, but "opus-4-5" narrows
* it to only that version.
*/
function familyHasSpecificEntries(
family: string,
allowlist: string[],
): boolean {
for (const entry of allowlist) {
if (isModelFamilyAlias(entry)) {
continue
}
// Check if entry is a version-qualified variant of this family
// e.g., "opus-4-5" or "claude-opus-4-5-20251101" for the "opus" family
// Must match at a segment boundary (followed by '-' or end) to avoid
// false positives like "opusplan" matching "opus"
const idx = entry.indexOf(family)
if (idx === -1) {
continue
}
const afterFamily = idx + family.length
if (afterFamily === entry.length || entry[afterFamily] === '-') {
return true
}
}
return false
}
/**
* Check if a model is allowed by the availableModels allowlist in settings.
* If availableModels is not set, all models are allowed.
*
* Matching tiers:
* 1. Family aliases ("opus", "sonnet", "haiku") — wildcard for the entire family,
* UNLESS more specific entries for that family also exist (e.g., "opus-4-5").
* In that case, the family wildcard is ignored and only the specific entries apply.
* 2. Version prefixes ("opus-4-5", "claude-opus-4-5") — any build of that version
* 3. Full model IDs ("claude-opus-4-5-20251101") — exact match only
*/
export function isModelAllowed(model: string): boolean {
const settings = getSettings_DEPRECATED() || {}
const { availableModels } = settings
if (!availableModels) {
return true // No restrictions
}
if (availableModels.length === 0) {
return false // Empty allowlist blocks all user-specified models
}
const resolvedModel = resolveOverriddenModel(model)
const normalizedModel = resolvedModel.trim().toLowerCase()
const normalizedAllowlist = availableModels.map(m => m.trim().toLowerCase())
// Direct match (alias-to-alias or full-name-to-full-name)
// Skip family aliases that have been narrowed by specific entries —
// e.g., "opus" in ["opus", "opus-4-5"] should NOT directly match,
// because the admin intends to restrict to opus 4.5 only.
if (normalizedAllowlist.includes(normalizedModel)) {
if (
!isModelFamilyAlias(normalizedModel) ||
!familyHasSpecificEntries(normalizedModel, normalizedAllowlist)
) {
return true
}
}
// Family-level aliases in the allowlist match any model in that family,
// but only if no more specific entries exist for that family.
// e.g., ["opus"] allows all opus, but ["opus", "opus-4-5"] only allows opus 4.5.
for (const entry of normalizedAllowlist) {
if (
isModelFamilyAlias(entry) &&
!familyHasSpecificEntries(entry, normalizedAllowlist) &&
modelBelongsToFamily(normalizedModel, entry)
) {
return true
}
}
// For non-family entries, do bidirectional alias resolution
// If model is an alias, resolve it and check if the resolved name is in the list
if (isModelAlias(normalizedModel)) {
const resolved = parseUserSpecifiedModel(normalizedModel).toLowerCase()
if (normalizedAllowlist.includes(resolved)) {
return true
}
}
// If any non-family alias in the allowlist resolves to the input model
for (const entry of normalizedAllowlist) {
if (!isModelFamilyAlias(entry) && isModelAlias(entry)) {
const resolved = parseUserSpecifiedModel(entry).toLowerCase()
if (resolved === normalizedModel) {
return true
}
}
}
// Version-prefix matching: "opus-4-5" or "claude-opus-4-5" matches
// "claude-opus-4-5-20251101" at a segment boundary
for (const entry of normalizedAllowlist) {
if (!isModelFamilyAlias(entry) && !isModelAlias(entry)) {
if (modelMatchesVersionPrefix(normalizedModel, entry)) {
return true
}
}
}
return false
}
|