mirror of
https://github.com/Wei-Shaw/claude-relay-service.git
synced 2026-01-22 16:43:35 +00:00
Merge branch 'lusipad/main'
This commit is contained in:
@@ -16,6 +16,22 @@ const {
|
||||
const tokenRefreshService = require('./tokenRefreshService')
|
||||
const LRUCache = require('../utils/lruCache')
|
||||
const { formatDateWithTimezone, getISOStringWithTimezone } = require('../utils/dateHelper')
|
||||
const { isOpus45OrNewer } = require('../utils/modelHelper')
|
||||
|
||||
/**
|
||||
* Check if account is Pro (not Max)
|
||||
* Compatible with both API real-time data (hasClaudePro) and local config (accountType)
|
||||
* @param {Object} info - Subscription info object
|
||||
* @returns {boolean}
|
||||
*/
|
||||
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 ClaudeAccountService {
|
||||
constructor() {
|
||||
@@ -852,31 +868,39 @@ class ClaudeAccountService {
|
||||
!this.isSubscriptionExpired(account)
|
||||
)
|
||||
|
||||
// 如果请求的是 Opus 模型,过滤掉 Pro 和 Free 账号
|
||||
// Filter Opus models based on account type and model version
|
||||
if (modelName && modelName.toLowerCase().includes('opus')) {
|
||||
const isNewOpus = isOpus45OrNewer(modelName)
|
||||
|
||||
activeAccounts = activeAccounts.filter((account) => {
|
||||
// 检查账号的订阅信息
|
||||
if (account.subscriptionInfo) {
|
||||
try {
|
||||
const info = JSON.parse(account.subscriptionInfo)
|
||||
// Pro 和 Free 账号不支持 Opus
|
||||
if (info.hasClaudePro === true && info.hasClaudeMax !== true) {
|
||||
return false // Claude Pro 不支持 Opus
|
||||
|
||||
// Free account: does not support any Opus model
|
||||
if (info.accountType === 'free') {
|
||||
return false
|
||||
}
|
||||
if (info.accountType === 'claude_pro' || info.accountType === 'claude_free') {
|
||||
return false // 明确标记为 Pro 或 Free 的账号不支持
|
||||
|
||||
// Pro account: only supports Opus 4.5+
|
||||
if (isProAccount(info)) {
|
||||
return isNewOpus
|
||||
}
|
||||
|
||||
// Max account: supports all Opus versions
|
||||
return true
|
||||
} catch (e) {
|
||||
// 解析失败,假设为旧数据,默认支持(兼容旧数据为 Max)
|
||||
// Parse failed, assume legacy data (Max), default support
|
||||
return true
|
||||
}
|
||||
}
|
||||
// 没有订阅信息的账号,默认当作支持(兼容旧数据)
|
||||
// Account without subscription info, default to supported (legacy data compatibility)
|
||||
return true
|
||||
})
|
||||
|
||||
if (activeAccounts.length === 0) {
|
||||
throw new Error('No Claude accounts available that support Opus model')
|
||||
const modelDesc = isNewOpus ? 'Opus 4.5+' : 'legacy Opus (requires Max subscription)'
|
||||
throw new Error(`No Claude accounts available that support ${modelDesc} model`)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -970,31 +994,39 @@ class ClaudeAccountService {
|
||||
!this.isSubscriptionExpired(account)
|
||||
)
|
||||
|
||||
// 如果请求的是 Opus 模型,过滤掉 Pro 和 Free 账号
|
||||
// Filter Opus models based on account type and model version
|
||||
if (modelName && modelName.toLowerCase().includes('opus')) {
|
||||
const isNewOpus = isOpus45OrNewer(modelName)
|
||||
|
||||
sharedAccounts = sharedAccounts.filter((account) => {
|
||||
// 检查账号的订阅信息
|
||||
if (account.subscriptionInfo) {
|
||||
try {
|
||||
const info = JSON.parse(account.subscriptionInfo)
|
||||
// Pro 和 Free 账号不支持 Opus
|
||||
if (info.hasClaudePro === true && info.hasClaudeMax !== true) {
|
||||
return false // Claude Pro 不支持 Opus
|
||||
|
||||
// Free account: does not support any Opus model
|
||||
if (info.accountType === 'free') {
|
||||
return false
|
||||
}
|
||||
if (info.accountType === 'claude_pro' || info.accountType === 'claude_free') {
|
||||
return false // 明确标记为 Pro 或 Free 的账号不支持
|
||||
|
||||
// Pro account: only supports Opus 4.5+
|
||||
if (isProAccount(info)) {
|
||||
return isNewOpus
|
||||
}
|
||||
|
||||
// Max account: supports all Opus versions
|
||||
return true
|
||||
} catch (e) {
|
||||
// 解析失败,假设为旧数据,默认支持(兼容旧数据为 Max)
|
||||
// Parse failed, assume legacy data (Max), default support
|
||||
return true
|
||||
}
|
||||
}
|
||||
// 没有订阅信息的账号,默认当作支持(兼容旧数据)
|
||||
// Account without subscription info, default to supported (legacy data compatibility)
|
||||
return true
|
||||
})
|
||||
|
||||
if (sharedAccounts.length === 0) {
|
||||
throw new Error('No shared Claude accounts available that support Opus model')
|
||||
const modelDesc = isNewOpus ? 'Opus 4.5+' : 'legacy Opus (requires Max subscription)'
|
||||
throw new Error(`No shared Claude accounts available that support ${modelDesc} model`)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -5,7 +5,33 @@ const ccrAccountService = require('./ccrAccountService')
|
||||
const accountGroupService = require('./accountGroupService')
|
||||
const redis = require('../models/redis')
|
||||
const logger = require('../utils/logger')
|
||||
const { parseVendorPrefixedModel } = require('../utils/modelHelper')
|
||||
const { parseVendorPrefixedModel, isOpus45OrNewer } = require('../utils/modelHelper')
|
||||
|
||||
/**
|
||||
* Check if account is Pro (not Max)
|
||||
*
|
||||
* ACCOUNT TYPE LOGIC (as of 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() {
|
||||
@@ -46,8 +72,14 @@ 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)
|
||||
|
||||
if (account.subscriptionInfo) {
|
||||
try {
|
||||
const info =
|
||||
@@ -55,27 +87,36 @@ class UnifiedClaudeScheduler {
|
||||
? JSON.parse(account.subscriptionInfo)
|
||||
: account.subscriptionInfo
|
||||
|
||||
// Pro 和 Free 账号不支持 Opus
|
||||
if (info.hasClaudePro === true && info.hasClaudeMax !== true) {
|
||||
// Free account: does not support any Opus model
|
||||
if (info.accountType === 'free') {
|
||||
logger.info(
|
||||
`🚫 Claude account ${account.name} (Pro) does not support Opus model${context ? ` ${context}` : ''}`
|
||||
`🚫 Claude account ${account.name} (Free) does not support Opus model${context ? ` ${context}` : ''}`
|
||||
)
|
||||
return false
|
||||
}
|
||||
if (info.accountType === 'claude_pro' || info.accountType === 'claude_free') {
|
||||
logger.info(
|
||||
`🚫 Claude account ${account.name} (${info.accountType}) does not support Opus model${context ? ` ${context}` : ''}`
|
||||
)
|
||||
return false
|
||||
|
||||
// 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+ supported
|
||||
return true
|
||||
}
|
||||
|
||||
// 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)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -70,9 +70,119 @@ function getVendorType(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
|
||||
getVendorType,
|
||||
isOpus45OrNewer
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user