π File detail
utils/model/modelAllowlist.ts
π§© .tsπ 171 linesπΎ 6,030 bytesπ text
β Back to All Filesπ― Use case
This file lives under βutils/β, which covers cross-cutting helpers (shell, tempfiles, settings, messages, process input, β¦). On the API surface it exposes isModelAllowed β mainly functions, hooks, or classes. It composes internal code from settings, aliases, model, and modelStrings (relative imports).
Generated from folder role, exports, dependency roots, and inline comments β not hand-reviewed for every path.
π§ Inline summary
import { getSettings_DEPRECATED } from '../settings/settings.js' import { isModelAlias, isModelFamilyAlias } from './aliases.js' import { parseUserSpecifiedModel } from './model.js' import { resolveOverriddenModel } from './modelStrings.js'
π€ Exports (heuristic)
isModelAllowed
π₯οΈ Source preview
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
}