init
This commit is contained in:
@@ -0,0 +1,170 @@
|
||||
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
|
||||
}
|
||||
Reference in New Issue
Block a user