mirror of
https://github.com/Wei-Shaw/claude-relay-service.git
synced 2026-01-22 16:43:35 +00:00
198 lines
6.3 KiB
JavaScript
198 lines
6.3 KiB
JavaScript
/**
|
||
* Model Helper Utility
|
||
*
|
||
* Provides utilities for parsing vendor-prefixed model names.
|
||
* Supports parsing model strings like "ccr,model_name" to extract vendor type and base model.
|
||
*/
|
||
|
||
// 仅保留原仓库既有的模型前缀:CCR 路由
|
||
// Gemini/Antigravity 采用“路径分流”,避免在 model 字段里混入 vendor 前缀造成混乱
|
||
const SUPPORTED_VENDOR_PREFIXES = ['ccr']
|
||
|
||
/**
|
||
* Parse vendor-prefixed model string
|
||
* @param {string} modelStr - Model string, potentially with vendor prefix (e.g., "ccr,gemini-2.5-pro")
|
||
* @returns {{vendor: string|null, baseModel: string}} - Parsed vendor and base model
|
||
*/
|
||
function parseVendorPrefixedModel(modelStr) {
|
||
if (!modelStr || typeof modelStr !== 'string') {
|
||
return { vendor: null, baseModel: modelStr || '' }
|
||
}
|
||
|
||
// Trim whitespace and convert to lowercase for comparison
|
||
const trimmed = modelStr.trim()
|
||
const lowerTrimmed = trimmed.toLowerCase()
|
||
|
||
for (const vendorPrefix of SUPPORTED_VENDOR_PREFIXES) {
|
||
if (!lowerTrimmed.startsWith(`${vendorPrefix},`)) {
|
||
continue
|
||
}
|
||
|
||
const parts = trimmed.split(',')
|
||
if (parts.length < 2) {
|
||
break
|
||
}
|
||
|
||
// Extract base model (everything after the first comma, rejoined in case model name contains commas)
|
||
const baseModel = parts.slice(1).join(',').trim()
|
||
return {
|
||
vendor: vendorPrefix,
|
||
baseModel
|
||
}
|
||
}
|
||
|
||
// No recognized vendor prefix found
|
||
return {
|
||
vendor: null,
|
||
baseModel: trimmed
|
||
}
|
||
}
|
||
|
||
/**
|
||
* Check if a model string has a vendor prefix
|
||
* @param {string} modelStr - Model string to check
|
||
* @returns {boolean} - True if the model has a vendor prefix
|
||
*/
|
||
function hasVendorPrefix(modelStr) {
|
||
const { vendor } = parseVendorPrefixedModel(modelStr)
|
||
return vendor !== null
|
||
}
|
||
|
||
/**
|
||
* Get the effective model name for scheduling and processing
|
||
* This removes vendor prefixes to get the actual model name used for API calls
|
||
* @param {string} modelStr - Original model string
|
||
* @returns {string} - Effective model name without vendor prefix
|
||
*/
|
||
function getEffectiveModel(modelStr) {
|
||
const { baseModel } = parseVendorPrefixedModel(modelStr)
|
||
return baseModel
|
||
}
|
||
|
||
/**
|
||
* Get the vendor type from a model string
|
||
* @param {string} modelStr - Model string to parse
|
||
* @returns {string|null} - Vendor type ('ccr') or null if no prefix
|
||
*/
|
||
function getVendorType(modelStr) {
|
||
const { vendor } = parseVendorPrefixedModel(modelStr)
|
||
return vendor
|
||
}
|
||
|
||
/**
|
||
* Check if the model is Opus 4.5 or newer.
|
||
*
|
||
* VERSION LOGIC (as of 2025-12-05):
|
||
* - Opus 4.5+ (including 5.0, 6.0, etc.) → returns true (Pro account eligible)
|
||
* - Opus 4.4 and below (including 3.x, 4.0, 4.1) → returns false (Max account only)
|
||
*
|
||
* Supported naming formats:
|
||
* - New format: claude-opus-{major}[-{minor}][-date], e.g., claude-opus-4-5-20251101
|
||
* - New format: claude-opus-{major}.{minor}, e.g., claude-opus-4.5
|
||
* - Old format: claude-{version}-opus[-date], e.g., claude-3-opus-20240229
|
||
* - Special: opus-latest, claude-opus-latest → always returns true
|
||
*
|
||
* @param {string} modelName - Model name
|
||
* @returns {boolean} - Whether the model is Opus 4.5 or newer
|
||
*/
|
||
function isOpus45OrNewer(modelName) {
|
||
if (!modelName) {
|
||
return false
|
||
}
|
||
|
||
const lowerModel = modelName.toLowerCase()
|
||
if (!lowerModel.includes('opus')) {
|
||
return false
|
||
}
|
||
|
||
// Handle 'latest' special case
|
||
if (lowerModel.includes('opus-latest') || lowerModel.includes('opus_latest')) {
|
||
return true
|
||
}
|
||
|
||
// Old format: claude-{version}-opus (version before opus)
|
||
// e.g., claude-3-opus-20240229, claude-3.5-opus
|
||
const oldFormatMatch = lowerModel.match(/claude[- ](\d+)(?:[.-](\d+))?[- ]opus/)
|
||
if (oldFormatMatch) {
|
||
const majorVersion = parseInt(oldFormatMatch[1], 10)
|
||
const minorVersion = oldFormatMatch[2] ? parseInt(oldFormatMatch[2], 10) : 0
|
||
|
||
// Old format version refers to Claude major version
|
||
// majorVersion > 4: 5.x, 6.x, ... → true
|
||
// majorVersion === 4 && minorVersion >= 5: 4.5, 4.6, ... → true
|
||
// Others (3.x, 4.0-4.4): → false
|
||
if (majorVersion > 4) {
|
||
return true
|
||
}
|
||
if (majorVersion === 4 && minorVersion >= 5) {
|
||
return true
|
||
}
|
||
return false
|
||
}
|
||
|
||
// New format 1: opus-{major}.{minor} (dot-separated)
|
||
// e.g., claude-opus-4.5, opus-4.5
|
||
const dotFormatMatch = lowerModel.match(/opus[- ]?(\d+)\.(\d+)/)
|
||
if (dotFormatMatch) {
|
||
const majorVersion = parseInt(dotFormatMatch[1], 10)
|
||
const minorVersion = parseInt(dotFormatMatch[2], 10)
|
||
|
||
// Same version logic as old format
|
||
// opus-5.0, opus-6.0 → true
|
||
// opus-4.5, opus-4.6 → true
|
||
// opus-4.0, opus-4.4 → false
|
||
if (majorVersion > 4) {
|
||
return true
|
||
}
|
||
if (majorVersion === 4 && minorVersion >= 5) {
|
||
return true
|
||
}
|
||
return false
|
||
}
|
||
|
||
// New format 2: opus-{major}[-{minor}][-date] (hyphen-separated)
|
||
// e.g., claude-opus-4-5-20251101, claude-opus-4-20250514, claude-opus-4-1-20250805
|
||
// If opus-{major} is followed by 8-digit date, there's no minor version
|
||
|
||
// Extract content after 'opus'
|
||
const opusIndex = lowerModel.indexOf('opus')
|
||
const afterOpus = lowerModel.substring(opusIndex + 4)
|
||
|
||
// Match: -{major}-{minor}-{date} or -{major}-{date} or -{major}
|
||
// IMPORTANT: Minor version regex is (\d{1,2}) not (\d+)
|
||
// This prevents matching 8-digit dates as minor version
|
||
// Example: opus-4-20250514 → major=4, minor=undefined (not 20250514)
|
||
// Example: opus-4-5-20251101 → major=4, minor=5
|
||
// Future-proof: Supports up to 2-digit minor versions (0-99)
|
||
const versionMatch = afterOpus.match(/^[- ](\d+)(?:[- ](\d{1,2})(?=[- ]\d{8}|$))?/)
|
||
|
||
if (versionMatch) {
|
||
const majorVersion = parseInt(versionMatch[1], 10)
|
||
const minorVersion = versionMatch[2] ? parseInt(versionMatch[2], 10) : 0
|
||
|
||
// Same version logic: >= 4.5 returns true
|
||
// opus-5-0-date, opus-6-date → true
|
||
// opus-4-5-date, opus-4-10-date → true (supports 2-digit minor)
|
||
// opus-4-date (no minor, treated as 4.0) → false
|
||
// opus-4-1-date, opus-4-4-date → false
|
||
if (majorVersion > 4) {
|
||
return true
|
||
}
|
||
if (majorVersion === 4 && minorVersion >= 5) {
|
||
return true
|
||
}
|
||
return false
|
||
}
|
||
|
||
// Other cases containing 'opus' but cannot parse version, assume legacy
|
||
return false
|
||
}
|
||
|
||
module.exports = {
|
||
parseVendorPrefixedModel,
|
||
hasVendorPrefix,
|
||
getEffectiveModel,
|
||
getVendorType,
|
||
isOpus45OrNewer
|
||
}
|