mirror of
https://github.com/Wei-Shaw/claude-relay-service.git
synced 2026-01-22 16:43:35 +00:00
fix(opus): fix PR#762 review issues and add maintenance comments
- Fix regex to support 2-digit minor versions (e.g., opus-4-10)
- Prevent matching 8-digit dates as minor version numbers
- Unify English comments for consistency across codebase
- Extract isProAccount() helper to eliminate code duplication
- Add detailed version logic comments for future maintenance
Changes:
- VERSION LOGIC: Opus 4.5+ returns true (Pro eligible), <4.5 returns false (Max only)
- ACCOUNT RESTRICTIONS: Free=no Opus, Pro=Opus 4.5+, Max=all Opus versions
- REGEX FIX: (\d{1,2}) limits minor version to 1-2 digits, avoiding date confusion
Test: All 21 tests pass
Format: Prettier validated
This commit is contained in:
@@ -7,6 +7,32 @@ const redis = require('../models/redis')
|
||||
const logger = require('../utils/logger')
|
||||
const { parseVendorPrefixedModel, isOpus45OrNewer } = require('../utils/modelHelper')
|
||||
|
||||
/**
|
||||
* Check if account is Pro (not Max)
|
||||
*
|
||||
* ACCOUNT TYPE判断逻辑 (2025-12-05):
|
||||
* Pro accounts can be identified by either:
|
||||
* 1. API real-time data: hasClaudePro=true && hasClaudeMax=false
|
||||
* 2. Local config data: accountType='claude_pro'
|
||||
*
|
||||
* Account type restrictions for Opus models:
|
||||
* - Free account: No Opus access at all
|
||||
* - Pro account: Only Opus 4.5+ (new versions)
|
||||
* - Max account: All Opus versions (legacy 3.x, 4.0, 4.1 and new 4.5+)
|
||||
*
|
||||
* Compatible with both API real-time data (hasClaudePro) and local config (accountType)
|
||||
* @param {Object} info - Subscription info object
|
||||
* @returns {boolean} - true if Pro account (not Free, not Max)
|
||||
*/
|
||||
function isProAccount(info) {
|
||||
// API real-time status takes priority
|
||||
if (info.hasClaudePro === true && info.hasClaudeMax !== true) {
|
||||
return true
|
||||
}
|
||||
// Local configured account type
|
||||
return info.accountType === 'claude_pro'
|
||||
}
|
||||
|
||||
class UnifiedClaudeScheduler {
|
||||
constructor() {
|
||||
this.SESSION_MAPPING_PREFIX = 'unified_claude_session_mapping:'
|
||||
@@ -46,7 +72,11 @@ class UnifiedClaudeScheduler {
|
||||
return false
|
||||
}
|
||||
|
||||
// 2. Opus 模型的订阅级别检查
|
||||
// 2. Opus model subscription level check
|
||||
// VERSION RESTRICTION LOGIC:
|
||||
// - Free: No Opus models
|
||||
// - Pro: Only Opus 4.5+ (isOpus45OrNewer = true)
|
||||
// - Max: All Opus versions
|
||||
if (requestedModel.toLowerCase().includes('opus')) {
|
||||
const isNewOpus = isOpus45OrNewer(requestedModel)
|
||||
|
||||
@@ -57,7 +87,7 @@ class UnifiedClaudeScheduler {
|
||||
? JSON.parse(account.subscriptionInfo)
|
||||
: account.subscriptionInfo
|
||||
|
||||
// Free 账号不支持任何 Opus 模型
|
||||
// Free account: does not support any Opus model
|
||||
if (info.accountType === 'free') {
|
||||
logger.info(
|
||||
`🚫 Claude account ${account.name} (Free) does not support Opus model${context ? ` ${context}` : ''}`
|
||||
@@ -65,37 +95,28 @@ class UnifiedClaudeScheduler {
|
||||
return false
|
||||
}
|
||||
|
||||
// Pro 账号:仅支持 Opus 4.5+
|
||||
if (info.hasClaudePro === true && info.hasClaudeMax !== true) {
|
||||
// Pro account: only supports Opus 4.5+
|
||||
// Reject legacy Opus (3.x, 4.0-4.4) but allow new Opus (4.5+)
|
||||
if (isProAccount(info)) {
|
||||
if (!isNewOpus) {
|
||||
logger.info(
|
||||
`🚫 Claude account ${account.name} (Pro) does not support legacy Opus model${context ? ` ${context}` : ''}`
|
||||
)
|
||||
return false
|
||||
}
|
||||
// Opus 4.5+ 支持
|
||||
return true
|
||||
}
|
||||
if (info.accountType === 'claude_pro') {
|
||||
if (!isNewOpus) {
|
||||
logger.info(
|
||||
`🚫 Claude account ${account.name} (Pro) does not support legacy Opus model${context ? ` ${context}` : ''}`
|
||||
)
|
||||
return false
|
||||
}
|
||||
// Opus 4.5+ 支持
|
||||
// Opus 4.5+ supported
|
||||
return true
|
||||
}
|
||||
|
||||
// Max 账号支持所有 Opus 版本
|
||||
// Max account: supports all Opus versions (no restriction)
|
||||
} catch (e) {
|
||||
// 解析失败,假设为旧数据(Max),默认支持
|
||||
// Parse failed, assume legacy data (Max), default support
|
||||
logger.debug(
|
||||
`Account ${account.name} has invalid subscriptionInfo${context ? ` ${context}` : ''}, assuming Max`
|
||||
)
|
||||
}
|
||||
}
|
||||
// 没有订阅信息的账号,默认当作支持(兼容旧数据)
|
||||
// Account without subscription info, default to supported (legacy data compatibility)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -71,14 +71,20 @@ function getVendorType(modelStr) {
|
||||
}
|
||||
|
||||
/**
|
||||
* 检查模型是否为 Opus 4.5 或更新版本
|
||||
* 支持格式:
|
||||
* - 新格式: claude-opus-{major}[-{minor}][-date] 如 claude-opus-4-5-20251101
|
||||
* - 新格式: claude-opus-{major}.{minor} 如 claude-opus-4.5
|
||||
* - 旧格式: claude-{version}-opus[-date] 如 claude-3-opus-20240229
|
||||
* Check if the model is Opus 4.5 or newer.
|
||||
*
|
||||
* @param {string} modelName - 模型名称
|
||||
* @returns {boolean} - 是否为 Opus 4.5+
|
||||
* VERSION判断逻辑 (2025-12-05):
|
||||
* - Opus 4.5+ (包括 5.0, 6.0 等) → 返回 true (Pro 账号可用)
|
||||
* - Opus 4.4 及以下 (包括 3.x, 4.0, 4.1) → 返回 false (仅 Max 账号可用)
|
||||
*
|
||||
* 支持的命名格式:
|
||||
* - 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) {
|
||||
@@ -90,19 +96,22 @@ function isOpus45OrNewer(modelName) {
|
||||
return false
|
||||
}
|
||||
|
||||
// 处理 latest 特殊情况
|
||||
// Handle 'latest' special case
|
||||
if (lowerModel.includes('opus-latest') || lowerModel.includes('opus_latest')) {
|
||||
return true
|
||||
}
|
||||
|
||||
// 旧格式: claude-{version}-opus (版本在 opus 前面)
|
||||
// 例如: claude-3-opus-20240229, claude-3.5-opus
|
||||
// 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
|
||||
|
||||
// 旧格式的版本号指的是 Claude 大版本
|
||||
// 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
|
||||
}
|
||||
@@ -112,13 +121,17 @@ function isOpus45OrNewer(modelName) {
|
||||
return false
|
||||
}
|
||||
|
||||
// 新格式 1: opus-{major}.{minor} (点分隔)
|
||||
// 例如: claude-opus-4.5, opus-4.5
|
||||
// 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
|
||||
}
|
||||
@@ -128,23 +141,31 @@ function isOpus45OrNewer(modelName) {
|
||||
return false
|
||||
}
|
||||
|
||||
// 新格式 2: opus-{major}[-{minor}][-date] (横线分隔)
|
||||
// 例如: claude-opus-4-5-20251101, claude-opus-4-20250514, claude-opus-4-1-20250805
|
||||
// 关键:小版本号必须是 1 位数字,且后面紧跟 8 位日期或结束
|
||||
// 如果 opus-{major} 后面直接是 8 位日期,则没有小版本号
|
||||
// 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
|
||||
|
||||
// 提取 opus 后面的部分
|
||||
// Extract content after 'opus'
|
||||
const opusIndex = lowerModel.indexOf('opus')
|
||||
const afterOpus = lowerModel.substring(opusIndex + 4) // 'opus' 后面的内容
|
||||
const afterOpus = lowerModel.substring(opusIndex + 4)
|
||||
|
||||
// 尝试匹配: -{major}-{minor}-{date} 或 -{major}-{date} 或 -{major}
|
||||
// 小版本号只能是 1 位数字 (如 1, 5),不会是 2 位以上
|
||||
const versionMatch = afterOpus.match(/^[- ](\d+)(?:[- ](\d)(?=[- ]\d{8}|$))?/)
|
||||
// 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
|
||||
}
|
||||
@@ -154,7 +175,7 @@ function isOpus45OrNewer(modelName) {
|
||||
return false
|
||||
}
|
||||
|
||||
// 其他包含 opus 但无法解析版本的情况,默认认为是旧版本
|
||||
// Other cases containing 'opus' but cannot parse version, assume legacy
|
||||
return false
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user