modelAllowlist.ts
utils/model/modelAllowlist.ts
171
Lines
6030
Bytes
1
Exports
4
Imports
10
Keywords
What this is
This page documents one file from the repository and includes its full source so you can read it without leaving the docs site.
Beginner explanation
This file is one piece of the larger system. Its name, directory, imports, and exports show where it fits. Start by reading the exports and related files first.
How it is used
Start from the exports list and related files. Those are the easiest clues for where this file fits into the system.
Expert explanation
Architecturally, this file intersects with modes. It contains 171 lines, 4 detected imports, and 1 detected exports.
Important relationships
Detected exports
isModelAllowed
Keywords
entrymodelfamilyopusopus-4-5allowlistnormalizedmodelcheckresolvednormalizedallowlist
Detected imports
../settings/settings.js./aliases.js./model.js./modelStrings.js
Source notes
This page embeds the full file contents. Small or leaf files are still indexed honestly instead of being over-explained.
Full source
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
}