import { and, eq, inArray, isNull } from 'drizzle-orm'; import { db, schema } from '../db/index.js'; import { upsertSetting } from '../db/upsertSetting.js'; import { config } from '../config.js'; import { getCachedModelRoutingReferenceCost, refreshModelPricingCatalog } from './modelPricingService.js'; import { RETRYABLE_TIMEOUT_PATTERNS } from './proxyRetryPolicy.js'; import { normalizeRouteRoutingStrategy, type RouteRoutingStrategy, } from './routeRoutingStrategy.js'; import { type DownstreamRoutingPolicy, EMPTY_DOWNSTREAM_ROUTING_POLICY } from './downstreamPolicyTypes.js'; import { isUsableAccountToken } from './accountTokenService.js'; import { getOauthInfoFromAccount } from './oauth/oauthAccount.js'; import { parseCodexQuotaResetHint } from './oauth/quota.js'; import { isExactTokenRouteModelPattern, isTokenRouteRegexPattern, matchesTokenRouteModelPattern, parseTokenRouteRegexPattern, } from '../../shared/tokenRoutePatterns.js'; import { normalizeTokenRouteMode, type RouteDecision, type RouteDecisionCandidate, type RouteMode, } from '../../shared/tokenRouteContract.js'; interface RouteMatch { route: RouteRow; channels: Array<{ channel: typeof schema.routeChannels.$inferSelect; account: typeof schema.accounts.$inferSelect; site: typeof schema.sites.$inferSelect; token: typeof schema.accountTokens.$inferSelect | null; }>; } type RouteChannelCandidate = RouteMatch['channels'][number]; interface SelectedChannel { channel: typeof schema.routeChannels.$inferSelect; account: typeof schema.accounts.$inferSelect; site: typeof schema.sites.$inferSelect; token: typeof schema.accountTokens.$inferSelect | null; tokenValue: string; tokenName: string; actualModel: string; } type FailureAwareChannel = { failCount?: number | null; lastFailAt?: string | null; }; type SiteRuntimeFailureContext = { status?: number | null; errorText?: string | null; modelName?: string | null; }; type SiteRuntimeHealthState = { penaltyScore: number; latencyEmaMs: number | null; transientFailureStreak: number; lastTransientFailureAtMs: number | null; breakerLevel: number; breakerUntilMs: number | null; lastUpdatedAtMs: number; lastFailureAtMs: number | null; lastSuccessAtMs: number | null; }; const FAILURE_BACKOFF_BASE_SEC = 15; const SHORT_WINDOW_LIMIT_COOLDOWN_MS = 5 * 60 * 1000; const MIN_EFFECTIVE_UNIT_COST = 1e-6; const ROUND_ROBIN_FAILURE_THRESHOLD = 3; const ROUND_ROBIN_COOLDOWN_LEVELS_SEC = [0, 10 * 60, 60 * 60, 24 * 60 * 60] as const; const SITE_RUNTIME_HEALTH_DECAY_HALF_LIFE_MS = 10 * 60 * 1000; const SITE_RUNTIME_MIN_MULTIPLIER = 0.08; const SITE_RUNTIME_LATENCY_BASELINE_MS = 2_500; const SITE_RUNTIME_LATENCY_WINDOW_MS = 30_000; const SITE_RUNTIME_MAX_LATENCY_PENALTY = 0.35; const SITE_RUNTIME_LATENCY_EMA_ALPHA = 0.3; const SITE_RUNTIME_BREAKER_STREAK_THRESHOLD = 3; const SITE_RUNTIME_BREAKER_LEVELS_MS = [0, 60_000, 5 * 60_000, 30 * 60 * 1000] as const; const SITE_TRANSIENT_STREAK_WINDOW_MS = 5 * 60 * 1000; const SITE_HISTORICAL_HEALTH_MIN_MULTIPLIER = 0.45; const SITE_HISTORICAL_HEALTH_MAX_SAMPLE = 24; const SITE_HISTORICAL_LATENCY_BASELINE_MS = 2_000; const SITE_HISTORICAL_LATENCY_WINDOW_MS = 20_000; const SITE_HISTORICAL_MAX_LATENCY_PENALTY = 0.18; const SITE_RUNTIME_HEALTH_SETTING_KEY = 'token_router_site_runtime_health_v1'; const SITE_RUNTIME_HEALTH_PERSIST_DEBOUNCE_MS = 500; const SITE_RUNTIME_HEALTH_PERSIST_STALE_TTL_MS = 7 * 24 * 60 * 60 * 1000; const SITE_RUNTIME_HEALTH_PERSIST_IDLE_TTL_MS = 12 * 60 * 60 * 1000; const SITE_RUNTIME_HEALTH_PERSIST_MIN_PENALTY = 0.02; const SITE_PROTOCOL_FAILURE_PATTERNS: RegExp[] = [ /unsupported\s+legacy\s+protocol/i, /please\s+use\s+\/v1\/responses/i, /please\s+use\s+\/v1\/messages/i, /please\s+use\s+\/v1\/chat\/completions/i, /does\s+not\s+allow\s+\/v1\/[a-z0-9/_:-]+\s+dispatch/i, /unsupported\s+endpoint/i, /unsupported\s+path/i, /unknown\s+endpoint/i, /unrecognized\s+request\s+url/i, /no\s+route\s+matched/i, ]; const SITE_MODEL_FAILURE_PATTERNS: RegExp[] = [ /unsupported\s+model/i, /model\s+not\s+supported/i, /does\s+not\s+support(?:\s+the)?\s+model/i, /no\s+such\s+model/i, /unknown\s+model/i, /invalid\s+model/i, /model.*does\s+not\s+exist/i, /当前\s*api\s*不支持所选模型/i, /不支持所选模型/i, ]; const SITE_VALIDATION_FAILURE_PATTERNS: RegExp[] = [ /invalid\s+request\s+body/i, /validation/i, /missing\s+required/i, /required\s+parameter/i, /unknown\s+parameter/i, /unrecognized\s+(field|key|parameter)/i, /malformed/i, /invalid\s+json/i, /cannot\s+parse/i, /unsupported\s+media\s+type/i, ]; const SITE_TRANSIENT_FAILURE_PATTERNS: RegExp[] = [ /bad\s+gateway/i, /gateway\s+time-?out/i, ...RETRYABLE_TIMEOUT_PATTERNS, /service\s+unavailable/i, /temporar(?:y|ily)\s+unavailable/i, /cpu\s+overloaded/i, /overloaded/i, /connection\s+reset/i, /connection\s+refused/i, /econnreset/i, /econnrefused/i, ]; const USAGE_LIMIT_RATE_LIMIT_PATTERNS: RegExp[] = [ /usage_limit_reached/i, /usage\s+limit\s+has\s+been\s+reached/i, /quota\s+exceeded/i, /rate\s+limit/i, /\blimit\b/i, ]; type SiteRuntimeHealthPersistencePayload = { version: 1; savedAtMs: number; globalBySiteId: Record; modelBySiteId: Record>; }; type SiteRuntimeHealthDetails = { globalMultiplier: number; modelMultiplier: number; combinedMultiplier: number; globalBreakerOpen: boolean; modelBreakerOpen: boolean; modelKey: string; }; type WeightedSelectionMode = 'weighted' | 'stable_first'; const siteRuntimeHealthStates = new Map(); const siteModelRuntimeHealthStates = new Map>(); let siteRuntimeHealthLoaded = false; let siteRuntimeHealthLoadPromise: Promise | null = null; let siteRuntimeHealthSaveTimer: ReturnType | null = null; let siteRuntimeHealthPersistInFlight: Promise | null = null; function fibonacciNumber(index: number): number { if (index <= 2) return 1; let prev = 1; let current = 1; for (let i = 3; i <= index; i += 1) { const next = prev + current; prev = current; current = next; } return current; } function resolveFailureBackoffSec(failCount?: number | null): number { const normalizedFailCount = Math.max(1, Math.trunc(failCount ?? 0)); return FAILURE_BACKOFF_BASE_SEC * fibonacciNumber(normalizedFailCount); } function resolveRoundRobinCooldownSec(level: number): number { const normalizedLevel = Math.max(0, Math.min(ROUND_ROBIN_COOLDOWN_LEVELS_SEC.length - 1, Math.trunc(level))); return ROUND_ROBIN_COOLDOWN_LEVELS_SEC[normalizedLevel] ?? 0; } function resolveSiteRuntimeBreakerMs(level: number): number { const normalizedLevel = Math.max(0, Math.min(SITE_RUNTIME_BREAKER_LEVELS_MS.length - 1, Math.trunc(level))); return SITE_RUNTIME_BREAKER_LEVELS_MS[normalizedLevel] ?? 0; } function matchesAnyPattern(patterns: RegExp[], input?: string | null): boolean { const text = (input || '').trim(); if (!text) return false; return patterns.some((pattern) => pattern.test(text)); } function isUsageLimitRateLimitFailure(context: SiteRuntimeFailureContext = {}): boolean { const status = typeof context.status === 'number' ? context.status : 0; if (status !== 429) return false; return matchesAnyPattern(USAGE_LIMIT_RATE_LIMIT_PATTERNS, context.errorText); } function clampNumber(value: number, min: number, max: number): number { return Math.max(min, Math.min(max, value)); } function isRecord(value: unknown): value is Record { return !!value && typeof value === 'object' && !Array.isArray(value); } function readFiniteNumber(value: unknown): number | null { return typeof value === 'number' && Number.isFinite(value) ? value : null; } function readFiniteInteger(value: unknown): number | null { const normalized = readFiniteNumber(value); return normalized == null ? null : Math.trunc(normalized); } function readNullableTimestamp(value: unknown): number | null { const normalized = readFiniteInteger(value); if (normalized == null || normalized <= 0) return null; return normalized; } function resolveSiteRuntimeFailurePenalty(context: SiteRuntimeFailureContext = {}): number { const status = typeof context.status === 'number' ? context.status : 0; const errorText = (context.errorText || '').trim(); if (isUsageLimitRateLimitFailure({ status, errorText })) { return 0.4; } if (status >= 500 || matchesAnyPattern(SITE_TRANSIENT_FAILURE_PATTERNS, errorText)) { return 2.5; } if (status === 429) { return 2.2; } if (status === 401 || status === 403) { return 1.8; } if (matchesAnyPattern(SITE_PROTOCOL_FAILURE_PATTERNS, errorText)) { return 0.6; } if (matchesAnyPattern(SITE_MODEL_FAILURE_PATTERNS, errorText)) { return 0.9; } if (matchesAnyPattern(SITE_VALIDATION_FAILURE_PATTERNS, errorText)) { return 0.25; } if (status >= 400 && status < 500) { return 0.9; } return 1.2; } function isTransientSiteRuntimeFailure(context: SiteRuntimeFailureContext = {}): boolean { const status = typeof context.status === 'number' ? context.status : 0; const errorText = (context.errorText || '').trim(); if (isUsageLimitRateLimitFailure({ status, errorText })) { return false; } return status >= 500 || status === 429 || matchesAnyPattern(SITE_TRANSIENT_FAILURE_PATTERNS, errorText); } function resolveShortWindowLimitCooldown( account: typeof schema.accounts.$inferSelect, context: SiteRuntimeFailureContext = {}, nowMs = Date.now(), ): string | null { const status = typeof context.status === 'number' ? context.status : 0; const errorText = (context.errorText || '').trim(); if (!isUsageLimitRateLimitFailure({ status, errorText })) return null; const resetHint = parseCodexQuotaResetHint(status, errorText, nowMs); if (resetHint) { const hintMs = Date.parse(resetHint.resetAt); if (Number.isFinite(hintMs) && hintMs > nowMs) { return new Date(hintMs).toISOString(); } } const oauth = getOauthInfoFromAccount(account); const storedResetAt = oauth?.quota?.lastLimitResetAt; if (oauth?.provider === 'codex' && storedResetAt) { const storedMs = Date.parse(storedResetAt); if (Number.isFinite(storedMs) && storedMs > nowMs) { return new Date(storedMs).toISOString(); } } return new Date(nowMs + SHORT_WINDOW_LIMIT_COOLDOWN_MS).toISOString(); } async function loadCredentialScopedChannelIds( channel: typeof schema.routeChannels.$inferSelect, accountId: number, ): Promise { if (typeof channel.tokenId === 'number' && channel.tokenId > 0) { const rows = await db.select({ id: schema.routeChannels.id }) .from(schema.routeChannels) .where(eq(schema.routeChannels.tokenId, channel.tokenId)) .all(); return rows.map((row) => row.id); } const rows = await db.select({ id: schema.routeChannels.id }) .from(schema.routeChannels) .where(and( eq(schema.routeChannels.accountId, accountId), isNull(schema.routeChannels.tokenId), )) .all(); return rows.map((row) => row.id); } function getDecayedSiteRuntimePenalty(state: SiteRuntimeHealthState, nowMs: number): number { if (!Number.isFinite(state.penaltyScore) || state.penaltyScore <= 0) return 0; const elapsedMs = Math.max(0, nowMs - state.lastUpdatedAtMs); if (elapsedMs <= 0) return state.penaltyScore; const decayFactor = Math.pow(0.5, elapsedMs / SITE_RUNTIME_HEALTH_DECAY_HALF_LIFE_MS); return state.penaltyScore * decayFactor; } function hydrateSiteRuntimeHealthState(raw: unknown): SiteRuntimeHealthState | null { if (!isRecord(raw)) return null; const lastUpdatedAtMs = readFiniteInteger(raw.lastUpdatedAtMs) ?? Date.now(); return { penaltyScore: Math.max(0, readFiniteNumber(raw.penaltyScore) ?? 0), latencyEmaMs: readFiniteNumber(raw.latencyEmaMs), transientFailureStreak: Math.max(0, readFiniteInteger(raw.transientFailureStreak) ?? 0), lastTransientFailureAtMs: readNullableTimestamp(raw.lastTransientFailureAtMs), breakerLevel: Math.max(0, readFiniteInteger(raw.breakerLevel) ?? 0), breakerUntilMs: readNullableTimestamp(raw.breakerUntilMs), lastUpdatedAtMs: Math.max(0, lastUpdatedAtMs), lastFailureAtMs: readNullableTimestamp(raw.lastFailureAtMs), lastSuccessAtMs: readNullableTimestamp(raw.lastSuccessAtMs), }; } function cloneSiteRuntimeHealthState(state: SiteRuntimeHealthState): SiteRuntimeHealthState { return { penaltyScore: state.penaltyScore, latencyEmaMs: state.latencyEmaMs, transientFailureStreak: state.transientFailureStreak, lastTransientFailureAtMs: state.lastTransientFailureAtMs, breakerLevel: state.breakerLevel, breakerUntilMs: state.breakerUntilMs, lastUpdatedAtMs: state.lastUpdatedAtMs, lastFailureAtMs: state.lastFailureAtMs, lastSuccessAtMs: state.lastSuccessAtMs, }; } function getOrCreateRuntimeHealthState(states: Map, key: K, nowMs = Date.now()): SiteRuntimeHealthState { const existing = states.get(key); if (!existing) { const initial: SiteRuntimeHealthState = { penaltyScore: 0, latencyEmaMs: null, transientFailureStreak: 0, lastTransientFailureAtMs: null, breakerLevel: 0, breakerUntilMs: null, lastUpdatedAtMs: nowMs, lastFailureAtMs: null, lastSuccessAtMs: null, }; states.set(key, initial); return initial; } const nextPenalty = getDecayedSiteRuntimePenalty(existing, nowMs); if (nextPenalty !== existing.penaltyScore || existing.lastUpdatedAtMs !== nowMs) { existing.penaltyScore = nextPenalty; existing.lastUpdatedAtMs = nowMs; } return existing; } function getOrCreateSiteRuntimeHealthState(siteId: number, nowMs = Date.now()): SiteRuntimeHealthState { return getOrCreateRuntimeHealthState(siteRuntimeHealthStates, siteId, nowMs); } function getSiteModelRuntimeHealthState(siteId: number, modelName?: string | null): SiteRuntimeHealthState | null { const modelKey = normalizeModelAlias(modelName || ''); if (!modelKey) return null; return siteModelRuntimeHealthStates.get(siteId)?.get(modelKey) ?? null; } function getOrCreateSiteModelRuntimeHealthState( siteId: number, modelName?: string | null, nowMs = Date.now(), ): SiteRuntimeHealthState | null { const modelKey = normalizeModelAlias(modelName || ''); if (!modelKey) return null; let modelStates = siteModelRuntimeHealthStates.get(siteId); if (!modelStates) { modelStates = new Map(); siteModelRuntimeHealthStates.set(siteId, modelStates); } return getOrCreateRuntimeHealthState(modelStates, modelKey, nowMs); } function isRuntimeHealthBreakerOpen(state: SiteRuntimeHealthState | null | undefined, nowMs = Date.now()): boolean { if (!state) return false; return typeof state.breakerUntilMs === 'number' && state.breakerUntilMs > nowMs; } function getRuntimeHealthMultiplier(state: SiteRuntimeHealthState | null | undefined, nowMs = Date.now()): number { if (!state) return 1; if (isRuntimeHealthBreakerOpen(state, nowMs)) { return SITE_RUNTIME_MIN_MULTIPLIER; } const penaltyScore = getDecayedSiteRuntimePenalty(state, nowMs); const failurePenaltyFactor = 1 / (1 + penaltyScore); const latencyPenaltyRatio = state.latencyEmaMs == null ? 0 : clampNumber( (state.latencyEmaMs - SITE_RUNTIME_LATENCY_BASELINE_MS) / SITE_RUNTIME_LATENCY_WINDOW_MS, 0, 1, ); const latencyFactor = 1 - (latencyPenaltyRatio * SITE_RUNTIME_MAX_LATENCY_PENALTY); return clampNumber(failurePenaltyFactor * latencyFactor, SITE_RUNTIME_MIN_MULTIPLIER, 1); } function getSiteRuntimeHealthDetails(siteId: number, modelName?: string | null, nowMs = Date.now()): SiteRuntimeHealthDetails { const modelKey = normalizeModelAlias(modelName || ''); const globalState = siteRuntimeHealthStates.get(siteId); const modelState = modelKey ? getSiteModelRuntimeHealthState(siteId, modelKey) : null; const globalMultiplier = getRuntimeHealthMultiplier(globalState, nowMs); const modelMultiplier = modelState ? getRuntimeHealthMultiplier(modelState, nowMs) : 1; return { globalMultiplier, modelMultiplier, combinedMultiplier: clampNumber( globalMultiplier * modelMultiplier, SITE_RUNTIME_MIN_MULTIPLIER * SITE_RUNTIME_MIN_MULTIPLIER, 1, ), globalBreakerOpen: isRuntimeHealthBreakerOpen(globalState, nowMs), modelBreakerOpen: isRuntimeHealthBreakerOpen(modelState, nowMs), modelKey, }; } function applyRuntimeHealthFailure(state: SiteRuntimeHealthState, context: SiteRuntimeFailureContext = {}, nowMs = Date.now()): void { state.penaltyScore += resolveSiteRuntimeFailurePenalty(context); if (isTransientSiteRuntimeFailure(context)) { const lastTransientFailureAtMs = state.lastTransientFailureAtMs; const shouldContinueStreak = ( typeof lastTransientFailureAtMs === 'number' && (nowMs - lastTransientFailureAtMs) <= SITE_TRANSIENT_STREAK_WINDOW_MS ); state.transientFailureStreak = shouldContinueStreak ? state.transientFailureStreak + 1 : 1; state.lastTransientFailureAtMs = nowMs; if (state.transientFailureStreak >= SITE_RUNTIME_BREAKER_STREAK_THRESHOLD) { state.breakerLevel = Math.min(state.breakerLevel + 1, SITE_RUNTIME_BREAKER_LEVELS_MS.length - 1); const breakerMs = resolveSiteRuntimeBreakerMs(state.breakerLevel); state.breakerUntilMs = breakerMs > 0 ? nowMs + breakerMs : null; state.transientFailureStreak = 0; } } else { state.transientFailureStreak = 0; state.lastTransientFailureAtMs = null; } state.lastFailureAtMs = nowMs; } function applyRuntimeHealthSuccess(state: SiteRuntimeHealthState, latencyMs: number, nowMs = Date.now()): void { state.penaltyScore = Math.max(0, state.penaltyScore * 0.2 - 0.3); state.transientFailureStreak = 0; state.lastTransientFailureAtMs = null; state.breakerLevel = 0; state.breakerUntilMs = null; state.lastSuccessAtMs = nowMs; const normalizedLatencyMs = Math.max(0, Math.trunc(latencyMs)); state.latencyEmaMs = state.latencyEmaMs == null ? normalizedLatencyMs : (state.latencyEmaMs * (1 - SITE_RUNTIME_LATENCY_EMA_ALPHA)) + (normalizedLatencyMs * SITE_RUNTIME_LATENCY_EMA_ALPHA); } function shouldPersistSiteRuntimeHealthState(state: SiteRuntimeHealthState, nowMs = Date.now()): boolean { const lastTouchedAtMs = Math.max( state.lastUpdatedAtMs, state.lastFailureAtMs ?? 0, state.lastSuccessAtMs ?? 0, state.lastTransientFailureAtMs ?? 0, ); if ((nowMs - lastTouchedAtMs) > SITE_RUNTIME_HEALTH_PERSIST_STALE_TTL_MS) { return false; } if (isRuntimeHealthBreakerOpen(state, nowMs)) return true; if (getDecayedSiteRuntimePenalty(state, nowMs) >= SITE_RUNTIME_HEALTH_PERSIST_MIN_PENALTY) return true; if ((state.latencyEmaMs ?? 0) > 0) return true; return (nowMs - lastTouchedAtMs) <= SITE_RUNTIME_HEALTH_PERSIST_IDLE_TTL_MS; } function buildSiteRuntimeHealthPersistencePayload(nowMs = Date.now()): SiteRuntimeHealthPersistencePayload { const globalBySiteId: Record = {}; const modelBySiteId: Record> = {}; for (const [siteId, state] of siteRuntimeHealthStates.entries()) { if (!shouldPersistSiteRuntimeHealthState(state, nowMs)) continue; globalBySiteId[String(siteId)] = cloneSiteRuntimeHealthState(state); } for (const [siteId, modelStates] of siteModelRuntimeHealthStates.entries()) { const persistedModels: Record = {}; for (const [modelKey, state] of modelStates.entries()) { if (!shouldPersistSiteRuntimeHealthState(state, nowMs)) continue; persistedModels[modelKey] = cloneSiteRuntimeHealthState(state); } if (Object.keys(persistedModels).length > 0) { modelBySiteId[String(siteId)] = persistedModels; } } return { version: 1, savedAtMs: nowMs, globalBySiteId, modelBySiteId, }; } async function persistSiteRuntimeHealthState(): Promise { if (siteRuntimeHealthPersistInFlight) { await siteRuntimeHealthPersistInFlight; return; } const persistTask = (async () => { const payload = buildSiteRuntimeHealthPersistencePayload(); await upsertSetting(SITE_RUNTIME_HEALTH_SETTING_KEY, payload); })(); siteRuntimeHealthPersistInFlight = persistTask.finally(() => { if (siteRuntimeHealthPersistInFlight === persistTask) { siteRuntimeHealthPersistInFlight = null; } }); await siteRuntimeHealthPersistInFlight; } function scheduleSiteRuntimeHealthPersistence(): void { if (siteRuntimeHealthSaveTimer) return; siteRuntimeHealthSaveTimer = setTimeout(() => { siteRuntimeHealthSaveTimer = null; void persistSiteRuntimeHealthState().catch((error) => { console.error('Failed to persist site runtime health state', error); }); }, SITE_RUNTIME_HEALTH_PERSIST_DEBOUNCE_MS); } async function loadSiteRuntimeHealthStateFromSettings(): Promise { siteRuntimeHealthStates.clear(); siteModelRuntimeHealthStates.clear(); const row = await db.select({ value: schema.settings.value }) .from(schema.settings) .where(eq(schema.settings.key, SITE_RUNTIME_HEALTH_SETTING_KEY)) .get(); if (!row?.value) return; let parsed: unknown; try { parsed = JSON.parse(row.value); } catch { return; } if (!isRecord(parsed)) return; const globalBySiteId = isRecord(parsed.globalBySiteId) ? parsed.globalBySiteId : {}; for (const [siteIdKey, stateRaw] of Object.entries(globalBySiteId)) { const siteId = Number(siteIdKey); if (!Number.isFinite(siteId) || siteId <= 0) continue; const state = hydrateSiteRuntimeHealthState(stateRaw); if (!state) continue; siteRuntimeHealthStates.set(siteId, state); } const modelBySiteId = isRecord(parsed.modelBySiteId) ? parsed.modelBySiteId : {}; for (const [siteIdKey, modelStatesRaw] of Object.entries(modelBySiteId)) { const siteId = Number(siteIdKey); if (!Number.isFinite(siteId) || siteId <= 0 || !isRecord(modelStatesRaw)) continue; const hydratedModelStates = new Map(); for (const [rawModelKey, stateRaw] of Object.entries(modelStatesRaw)) { const modelKey = normalizeModelAlias(rawModelKey); if (!modelKey) continue; const state = hydrateSiteRuntimeHealthState(stateRaw); if (!state) continue; hydratedModelStates.set(modelKey, state); } if (hydratedModelStates.size > 0) { siteModelRuntimeHealthStates.set(siteId, hydratedModelStates); } } } async function ensureSiteRuntimeHealthStateLoaded(): Promise { if (siteRuntimeHealthLoaded) return; if (!siteRuntimeHealthLoadPromise) { siteRuntimeHealthLoadPromise = (async () => { try { await loadSiteRuntimeHealthStateFromSettings(); siteRuntimeHealthLoaded = true; } catch (error) { console.warn('Failed to restore site runtime health state from settings', error); siteRuntimeHealthLoadPromise = null; siteRuntimeHealthLoaded = false; } })(); } await siteRuntimeHealthLoadPromise; } function recordSiteRuntimeFailure(siteId: number, context: SiteRuntimeFailureContext = {}, nowMs = Date.now()): void { applyRuntimeHealthFailure(getOrCreateSiteRuntimeHealthState(siteId, nowMs), context, nowMs); const modelState = getOrCreateSiteModelRuntimeHealthState(siteId, context.modelName, nowMs); if (modelState) { applyRuntimeHealthFailure(modelState, context, nowMs); } scheduleSiteRuntimeHealthPersistence(); } function recordSiteRuntimeSuccess(siteId: number, latencyMs: number, modelName?: string | null, nowMs = Date.now()): void { applyRuntimeHealthSuccess(getOrCreateSiteRuntimeHealthState(siteId, nowMs), latencyMs, nowMs); const modelState = getOrCreateSiteModelRuntimeHealthState(siteId, modelName, nowMs); if (modelState) { applyRuntimeHealthSuccess(modelState, latencyMs, nowMs); } scheduleSiteRuntimeHealthPersistence(); } export function resetSiteRuntimeHealthState(): void { siteRuntimeHealthStates.clear(); siteModelRuntimeHealthStates.clear(); siteRuntimeHealthLoaded = false; siteRuntimeHealthLoadPromise = null; if (siteRuntimeHealthSaveTimer) { clearTimeout(siteRuntimeHealthSaveTimer); siteRuntimeHealthSaveTimer = null; } siteRuntimeHealthPersistInFlight = null; } export async function flushSiteRuntimeHealthPersistence(): Promise { if (siteRuntimeHealthSaveTimer) { clearTimeout(siteRuntimeHealthSaveTimer); siteRuntimeHealthSaveTimer = null; await persistSiteRuntimeHealthState(); return; } if (siteRuntimeHealthPersistInFlight) { await siteRuntimeHealthPersistInFlight; } } export function getSiteRuntimeHealthMultiplier(siteId: number, nowMs = Date.now()): number { const state = siteRuntimeHealthStates.get(siteId); return getRuntimeHealthMultiplier(state, nowMs); } export function isSiteRuntimeBreakerOpen(siteId: number, nowMs = Date.now()): boolean { const state = siteRuntimeHealthStates.get(siteId); return isRuntimeHealthBreakerOpen(state, nowMs); } export function filterSiteRuntimeBrokenCandidates( candidates: T[], nowMs = Date.now(), ): T[] { if (candidates.length <= 1) return candidates; const healthy = candidates.filter((candidate) => !isSiteRuntimeBreakerOpen(candidate.site.id, nowMs)); return healthy.length > 0 ? healthy : candidates; } function buildRuntimeBreakerReason(details: SiteRuntimeHealthDetails): string { if (details.globalBreakerOpen && details.modelBreakerOpen) { return '站点熔断中,模型熔断中,优先避让'; } if (details.globalBreakerOpen) { return '站点熔断中,优先避让'; } if (details.modelBreakerOpen) { return '模型熔断中,优先避让'; } return '运行时熔断中,优先避让'; } function filterSiteRuntimeBrokenCandidatesByModel( candidates: RouteChannelCandidate[], modelName: string | ((candidate: RouteChannelCandidate) => string), nowMs = Date.now(), ): { candidates: RouteChannelCandidate[]; avoided: Array<{ candidate: RouteChannelCandidate; reason: string }>; } { if (candidates.length <= 1) { return { candidates, avoided: [], }; } const resolveModelName = typeof modelName === 'function' ? modelName : (() => modelName); const avoided: Array<{ candidate: RouteChannelCandidate; reason: string }> = []; const healthy = candidates.filter((candidate) => { const details = getSiteRuntimeHealthDetails(candidate.site.id, resolveModelName(candidate), nowMs); const blocked = details.globalBreakerOpen || details.modelBreakerOpen; if (blocked) { avoided.push({ candidate, reason: buildRuntimeBreakerReason(details), }); } return !blocked; }); return healthy.length > 0 ? { candidates: healthy, avoided, } : { candidates, avoided: [], }; } type RouteRow = typeof schema.tokenRoutes.$inferSelect & { routeMode: RouteMode; sourceRouteIds: number[]; }; type ChannelRow = typeof schema.routeChannels.$inferSelect; type RouteCacheSnapshot = { loadedAt: number; routes: RouteRow[]; }; type RouteMatchCacheSnapshot = { loadedAt: number; match: RouteMatch; }; let routeCacheSnapshot: RouteCacheSnapshot = { loadedAt: 0, routes: [], }; const routeMatchCache = new Map(); function resolveTokenRouterCacheTtlMs(): number { const raw = Math.trunc(config.tokenRouterCacheTtlMs || 0); return Math.max(100, raw); } function isCacheFresh(loadedAt: number, nowMs: number): boolean { return nowMs - loadedAt < resolveTokenRouterCacheTtlMs(); } async function loadEnabledRoutes(nowMs = Date.now()): Promise { if (isCacheFresh(routeCacheSnapshot.loadedAt, nowMs)) { return routeCacheSnapshot.routes; } const rawRoutes = await db.select().from(schema.tokenRoutes) .where(eq(schema.tokenRoutes.enabled, true)) .all(); const explicitGroupRouteIds = rawRoutes .filter((route) => normalizeRouteMode(route.routeMode) === 'explicit_group') .map((route) => route.id); const sourceRows = explicitGroupRouteIds.length > 0 ? await db.select().from(schema.routeGroupSources) .where(inArray(schema.routeGroupSources.groupRouteId, explicitGroupRouteIds)) .all() : []; const sourceIdsByRouteId = new Map(); for (const row of sourceRows) { if (!sourceIdsByRouteId.has(row.groupRouteId)) { sourceIdsByRouteId.set(row.groupRouteId, []); } sourceIdsByRouteId.get(row.groupRouteId)!.push(row.sourceRouteId); } const routes = rawRoutes.map((route) => ({ ...route, routeMode: normalizeRouteMode(route.routeMode), sourceRouteIds: Array.from(new Set(sourceIdsByRouteId.get(route.id) ?? [])), })); routeCacheSnapshot = { loadedAt: nowMs, routes, }; return routes; } async function loadRouteMatch(route: RouteRow, nowMs = Date.now()): Promise { const cached = routeMatchCache.get(route.id); if (cached && isCacheFresh(cached.loadedAt, nowMs)) { return cached.match; } const enabledRoutes = await loadEnabledRoutes(nowMs); const routeIds = (() => { if (!isExplicitGroupRoute(route)) { return [route.id]; } return Array.from(new Set(route.sourceRouteIds.filter((routeId) => Number.isFinite(routeId) && routeId > 0))); })(); const enabledSourceRoutes = isExplicitGroupRoute(route) ? enabledRoutes.filter((item) => ( routeIds.includes(item.id) && !isExplicitGroupRoute(item) && isExactRouteModelPattern(item.modelPattern) )) : enabledRoutes.filter((item) => routeIds.includes(item.id)); const enabledSourceRouteIds = enabledSourceRoutes.map((item) => item.id); const fallbackSourceModelByRouteId = new Map( enabledSourceRoutes .filter((item) => isExactRouteModelPattern(item.modelPattern)) .map((item) => [item.id, (item.modelPattern || '').trim()]), ); const channels = enabledSourceRouteIds.length > 0 ? await db .select() .from(schema.routeChannels) .innerJoin(schema.accounts, eq(schema.routeChannels.accountId, schema.accounts.id)) .innerJoin(schema.sites, eq(schema.accounts.siteId, schema.sites.id)) .leftJoin(schema.accountTokens, eq(schema.routeChannels.tokenId, schema.accountTokens.id)) .where(inArray(schema.routeChannels.routeId, enabledSourceRouteIds)) .all() : []; const mapped = channels.map((row) => ({ channel: { ...row.route_channels, sourceModel: normalizeChannelSourceModel(row.route_channels.sourceModel) || fallbackSourceModelByRouteId.get(row.route_channels.routeId) || null, }, account: row.accounts, site: row.sites, token: row.account_tokens, })); const match = { route, channels: mapped }; routeMatchCache.set(route.id, { loadedAt: nowMs, match, }); return match; } function patchCachedChannel(channelId: number, apply: (channel: ChannelRow) => void): void { for (const entry of routeMatchCache.values()) { const target = entry.match.channels.find((item) => item.channel.id === channelId); if (!target) continue; apply(target.channel); break; } } export function invalidateTokenRouterCache(): void { routeCacheSnapshot = { loadedAt: 0, routes: [], }; routeMatchCache.clear(); } function isSiteDisabled(status?: string | null): boolean { return (status || 'active') === 'disabled'; } export function isChannelRecentlyFailed( channel: FailureAwareChannel, nowMs = Date.now(), avoidSec = resolveFailureBackoffSec(channel.failCount), ): boolean { if (avoidSec <= 0) return false; if ((channel.failCount ?? 0) <= 0) return false; if (!channel.lastFailAt) return false; const failTs = Date.parse(channel.lastFailAt); if (Number.isNaN(failTs)) return false; return nowMs - failTs < avoidSec * 1000; } export function filterRecentlyFailedCandidates( candidates: T[], nowMs = Date.now(), avoidSec?: number, ): T[] { if (candidates.length <= 1) return candidates; if (avoidSec == null || avoidSec <= 0) return candidates; const healthy = candidates.filter((candidate) => !isChannelRecentlyFailed(candidate.channel, nowMs, avoidSec)); // If all channels failed recently, keep them all and let weight/random decide. return healthy.length > 0 ? healthy : candidates; } export type RouteDecisionExplanation = RouteDecision & { routeId?: number; modelPattern?: string; selectedAccountId?: number; }; const DEFAULT_DOWNSTREAM_POLICY: DownstreamRoutingPolicy = EMPTY_DOWNSTREAM_ROUTING_POLICY; type ExplainSelectionOptions = { excludeChannelIds?: number[]; bypassSourceModelCheck?: boolean; useChannelSourceModelForCost?: boolean; downstreamPolicy?: DownstreamRoutingPolicy; }; type PricingReferenceRefreshOptions = { useChannelSourceModelForCost?: boolean; downstreamPolicy?: DownstreamRoutingPolicy; refreshedKeys?: Set; }; type CandidateEligibilityOptions = { requestedModel: string; bypassSourceModelCheck?: boolean; excludeChannelIds?: number[]; nowIso?: string; }; type CostSignal = { unitCost: number; source: 'observed' | 'configured' | 'catalog' | 'fallback'; }; export function isRegexModelPattern(pattern: string): boolean { return isTokenRouteRegexPattern(pattern); } export function parseRegexModelPattern(pattern: string): { test(value: string): boolean } | null { return parseTokenRouteRegexPattern(pattern).regex; } export function matchesModelPattern(model: string, pattern: string): boolean { return matchesTokenRouteModelPattern(model, pattern); } function isExactRouteModelPattern(pattern: string): boolean { return isExactTokenRouteModelPattern(pattern); } function normalizeRouteMode(routeMode: string | null | undefined): RouteMode { return normalizeTokenRouteMode(routeMode); } function isExplicitGroupRoute(route: Pick | Pick): boolean { return normalizeRouteMode(route.routeMode) === 'explicit_group'; } function normalizeRouteDisplayName(displayName: string | null | undefined): string { return (displayName || '').trim(); } function isRouteDisplayNameMatch(model: string, displayName: string | null | undefined): boolean { const alias = normalizeRouteDisplayName(displayName); return !!alias && alias === model; } function matchesRouteRequestModel(model: string, route: RouteRow): boolean { if (isExplicitGroupRoute(route)) { return isRouteDisplayNameMatch(model, route.displayName); } return matchesModelPattern(model, route.modelPattern) || isRouteDisplayNameMatch(model, route.displayName); } function getExposedModelNameForRoute(route: RouteRow): string { return normalizeRouteDisplayName(route.displayName) || route.modelPattern; } function hasCustomDisplayName(route: Pick): boolean { const displayName = normalizeRouteDisplayName(route.displayName); const modelPattern = (route.modelPattern || '').trim(); return !!displayName && displayName !== modelPattern; } function buildVisibleEnabledRoutes(routes: RouteRow[]): RouteRow[] { const exactModelNames = new Set( routes .filter((route) => !isExplicitGroupRoute(route) && isExactRouteModelPattern(route.modelPattern)) .map((route) => (route.modelPattern || '').trim()) .filter(Boolean), ); const coveringGroups = routes.filter((route) => ( route.enabled && ( (isExplicitGroupRoute(route) && normalizeRouteDisplayName(route.displayName).length > 0 && route.sourceRouteIds.length > 0) || (!isExplicitGroupRoute(route) && !isExactRouteModelPattern(route.modelPattern) && hasCustomDisplayName(route)) ) )); if (coveringGroups.length === 0) return routes; return routes.filter((route) => { if (isExplicitGroupRoute(route)) { return normalizeRouteDisplayName(route.displayName).length > 0; } if (!isExactRouteModelPattern(route.modelPattern)) return true; if (hasCustomDisplayName(route)) return true; const exactModel = (route.modelPattern || '').trim(); if (!exactModel) return true; return !coveringGroups.some((groupRoute) => { if (groupRoute.id === route.id) return false; const groupDisplayName = normalizeRouteDisplayName(groupRoute.displayName); if (!groupDisplayName || exactModelNames.has(groupDisplayName)) return false; if (isExplicitGroupRoute(groupRoute)) { return groupRoute.sourceRouteIds.includes(route.id); } return matchesModelPattern(exactModel, groupRoute.modelPattern); }); }); } function normalizeModelAlias(modelName: string): string { const normalized = (modelName || '').trim().toLowerCase(); if (!normalized) return ''; const slashIndex = normalized.lastIndexOf('/'); if (slashIndex >= 0 && slashIndex < normalized.length - 1) { return normalized.slice(slashIndex + 1); } return normalized; } function isModelAliasEquivalent(left: string, right: string): boolean { const a = normalizeModelAlias(left); const b = normalizeModelAlias(right); return !!a && !!b && a === b; } function channelSupportsRequestedModel(channelSourceModel: string | null | undefined, requestedModel: string): boolean { const source = (channelSourceModel || '').trim(); if (!source) return true; if (source === requestedModel) return true; if (isModelAliasEquivalent(source, requestedModel)) return true; if (matchesModelPattern(requestedModel, source)) return true; return false; } function isModelAllowedByDownstreamPolicy(requestedModel: string, policy: DownstreamRoutingPolicy): boolean { const supportedPatterns = Array.isArray(policy.supportedModels) ? policy.supportedModels : []; const hasSupportedPatterns = supportedPatterns.length > 0; const hasAllowedRoutes = policy.allowedRouteIds.length > 0; if (!hasSupportedPatterns && !hasAllowedRoutes) return policy.denyAllWhenEmpty === true ? false : true; const matchedSupportedPattern = supportedPatterns.some((pattern) => matchesModelPattern(requestedModel, pattern)); if (matchedSupportedPattern) return true; if (hasAllowedRoutes) return true; return false; } function parseModelMappingRecord(modelMapping?: string | Record | null): Record | null { if (!modelMapping) return null; if (typeof modelMapping === 'object' && !Array.isArray(modelMapping)) { return modelMapping as Record; } if (typeof modelMapping !== 'string') return null; try { const parsed = JSON.parse(modelMapping); if (!parsed || typeof parsed !== 'object' || Array.isArray(parsed)) return null; return parsed as Record; } catch { return null; } } function resolveMappedModel(requestedModel: string, modelMapping?: string | Record | null): string { const parsed = parseModelMappingRecord(modelMapping); if (!parsed) return requestedModel; const entries = Object.entries(parsed) .filter(([, value]) => typeof value === 'string' && value.trim().length > 0) as Array<[string, string]>; const exact = entries.find(([pattern]) => pattern === requestedModel); if (exact) return exact[1].trim(); for (const [pattern, target] of entries) { if (matchesModelPattern(requestedModel, pattern)) { return target.trim(); } } return requestedModel; } function normalizeChannelSourceModel(channelSourceModel: string | null | undefined): string { return (channelSourceModel || '').trim(); } function resolveActualModelForSelectedChannel( requestedModel: string, route: RouteRow, mappedModel: string, channelSourceModel: string | null | undefined, ): string { const sourceModel = normalizeChannelSourceModel(channelSourceModel); if (isRouteDisplayNameMatch(requestedModel, route.displayName) && sourceModel) { return sourceModel; } return mappedModel; } function resolveRouteStrategy(route: RouteRow): RouteRoutingStrategy { return normalizeRouteRoutingStrategy(route.routingStrategy); } function parseIsoTimeMs(value?: string | null): number | null { if (!value) return null; const parsed = Date.parse(value); return Number.isNaN(parsed) ? null : parsed; } function compareNullableTimeAsc(left?: string | null, right?: string | null): number { const leftMs = parseIsoTimeMs(left); const rightMs = parseIsoTimeMs(right); if (leftMs == null && rightMs == null) return 0; if (leftMs == null) return -1; if (rightMs == null) return 1; return leftMs - rightMs; } function resolveEffectiveUnitCost(candidate: RouteChannelCandidate, modelName: string): CostSignal { const successCount = Math.max(0, candidate.channel.successCount ?? 0); const totalCost = Math.max(0, candidate.channel.totalCost ?? 0); const configured = candidate.account.unitCost ?? null; if (successCount > 0 && totalCost > 0) { return { unitCost: Math.max(totalCost / successCount, MIN_EFFECTIVE_UNIT_COST), source: 'observed', }; } if (typeof configured === 'number' && Number.isFinite(configured) && configured > 0) { return { unitCost: Math.max(configured, MIN_EFFECTIVE_UNIT_COST), source: 'configured', }; } const catalogCost = getCachedModelRoutingReferenceCost({ siteId: candidate.site.id, accountId: candidate.account.id, modelName, }); if (typeof catalogCost === 'number' && Number.isFinite(catalogCost) && catalogCost > 0) { return { unitCost: Math.max(catalogCost, MIN_EFFECTIVE_UNIT_COST), source: 'catalog', }; } return { unitCost: Math.max(config.routingFallbackUnitCost || 1, MIN_EFFECTIVE_UNIT_COST), source: 'fallback', }; } type SiteHistoricalHealthMetrics = { multiplier: number; totalCalls: number; successRate: number | null; avgLatencyMs: number | null; }; function buildSiteHistoricalHealthMetrics(candidates: RouteChannelCandidate[]): Map { const totals = new Map(); for (const candidate of candidates) { const siteId = candidate.site.id; if (!totals.has(siteId)) { totals.set(siteId, { totalCalls: 0, successCount: 0, failCount: 0, totalLatencyMs: 0, latencySamples: 0, }); } const target = totals.get(siteId)!; const successCount = Math.max(0, candidate.channel.successCount ?? 0); const failCount = Math.max(0, candidate.channel.failCount ?? 0); target.successCount += successCount; target.failCount += failCount; target.totalCalls += successCount + failCount; if (successCount > 0) { target.totalLatencyMs += Math.max(0, candidate.channel.totalLatencyMs ?? 0); target.latencySamples += successCount; } } const metrics = new Map(); for (const [siteId, total] of totals.entries()) { if (total.totalCalls <= 0) { metrics.set(siteId, { multiplier: 1, totalCalls: 0, successRate: null, avgLatencyMs: null, }); continue; } const sampleFactor = clampNumber(total.totalCalls / SITE_HISTORICAL_HEALTH_MAX_SAMPLE, 0, 1); const successRate = total.successCount / total.totalCalls; const successPenaltyFactor = 1 - ((1 - successRate) * 0.55 * sampleFactor); const avgLatencyMs = total.latencySamples > 0 ? Math.round(total.totalLatencyMs / total.latencySamples) : null; const latencyPenaltyRatio = avgLatencyMs == null ? 0 : clampNumber( (avgLatencyMs - SITE_HISTORICAL_LATENCY_BASELINE_MS) / SITE_HISTORICAL_LATENCY_WINDOW_MS, 0, 1, ) * sampleFactor; const latencyFactor = 1 - (latencyPenaltyRatio * SITE_HISTORICAL_MAX_LATENCY_PENALTY); metrics.set(siteId, { multiplier: clampNumber( successPenaltyFactor * latencyFactor, SITE_HISTORICAL_HEALTH_MIN_MULTIPLIER, 1, ), totalCalls: total.totalCalls, successRate, avgLatencyMs, }); } return metrics; } function isExplicitTokenChannel(candidate: RouteChannelCandidate): boolean { return typeof candidate.channel.tokenId === 'number' && candidate.channel.tokenId > 0; } export class TokenRouter { /** * Find matching route and select a channel for the given model. * Returns null if no route/channel available. */ async selectChannel(requestedModel: string, downstreamPolicy: DownstreamRoutingPolicy = DEFAULT_DOWNSTREAM_POLICY): Promise { if (!isModelAllowedByDownstreamPolicy(requestedModel, downstreamPolicy)) return null; await ensureSiteRuntimeHealthStateLoaded(); const match = await this.findRoute(requestedModel, downstreamPolicy); if (!match) return null; return await this.selectFromMatch(match, requestedModel, downstreamPolicy); } async previewSelectedChannel( requestedModel: string, downstreamPolicy: DownstreamRoutingPolicy = DEFAULT_DOWNSTREAM_POLICY, ): Promise { if (!isModelAllowedByDownstreamPolicy(requestedModel, downstreamPolicy)) return null; await ensureSiteRuntimeHealthStateLoaded(); const match = await this.findRoute(requestedModel, downstreamPolicy); if (!match) return null; return await this.selectFromMatch(match, requestedModel, downstreamPolicy, [], false); } /** * Select next channel for failover (exclude already-tried channels). */ async selectNextChannel( requestedModel: string, excludeChannelIds: number[], downstreamPolicy: DownstreamRoutingPolicy = DEFAULT_DOWNSTREAM_POLICY, ): Promise { if (!isModelAllowedByDownstreamPolicy(requestedModel, downstreamPolicy)) return null; await ensureSiteRuntimeHealthStateLoaded(); const match = await this.findRoute(requestedModel, downstreamPolicy); if (!match) return null; return await this.selectFromMatch(match, requestedModel, downstreamPolicy, excludeChannelIds); } async selectPreferredChannel( requestedModel: string, preferredChannelId: number, downstreamPolicy: DownstreamRoutingPolicy = DEFAULT_DOWNSTREAM_POLICY, excludeChannelIds: number[] = [], ): Promise { if (!isModelAllowedByDownstreamPolicy(requestedModel, downstreamPolicy)) return null; const normalizedPreferredChannelId = Math.trunc(preferredChannelId || 0); if (normalizedPreferredChannelId <= 0) return null; await ensureSiteRuntimeHealthStateLoaded(); const match = await this.findRoute(requestedModel, downstreamPolicy); if (!match) return null; return await this.selectPreferredFromMatch( match, requestedModel, normalizedPreferredChannelId, excludeChannelIds, ); } async explainSelection( requestedModel: string, excludeChannelIds: number[] = [], downstreamPolicy: DownstreamRoutingPolicy = DEFAULT_DOWNSTREAM_POLICY, ): Promise { await ensureSiteRuntimeHealthStateLoaded(); const match = await this.findRoute(requestedModel, downstreamPolicy); return this.explainSelectionFromMatch(match, requestedModel, { excludeChannelIds, downstreamPolicy }); } async explainSelectionForRoute( routeId: number, requestedModel: string, excludeChannelIds: number[] = [], downstreamPolicy: DownstreamRoutingPolicy = DEFAULT_DOWNSTREAM_POLICY, ): Promise { await ensureSiteRuntimeHealthStateLoaded(); const match = await this.findRouteById(routeId, downstreamPolicy); return this.explainSelectionFromMatch(match, requestedModel, { excludeChannelIds, downstreamPolicy }); } async explainSelectionRouteWide(routeId: number, downstreamPolicy: DownstreamRoutingPolicy = DEFAULT_DOWNSTREAM_POLICY): Promise { await ensureSiteRuntimeHealthStateLoaded(); const match = await this.findRouteById(routeId, downstreamPolicy); const fallbackRequestedModel = match?.route.modelPattern || `route:${routeId}`; return this.explainSelectionFromMatch(match, fallbackRequestedModel, { bypassSourceModelCheck: true, useChannelSourceModelForCost: true, downstreamPolicy, }); } async refreshPricingReferenceCosts( requestedModel: string, options: PricingReferenceRefreshOptions = {}, ): Promise { const downstreamPolicy = options.downstreamPolicy ?? DEFAULT_DOWNSTREAM_POLICY; const match = await this.findRoute(requestedModel, downstreamPolicy); await this.refreshPricingReferenceCostsForMatch(match, requestedModel, options); } async refreshPricingReferenceCostsForRoute( routeId: number, requestedModel: string, options: PricingReferenceRefreshOptions = {}, ): Promise { const downstreamPolicy = options.downstreamPolicy ?? DEFAULT_DOWNSTREAM_POLICY; const match = await this.findRouteById(routeId, downstreamPolicy); await this.refreshPricingReferenceCostsForMatch(match, requestedModel, options); } async refreshRouteWidePricingReferenceCosts( routeId: number, options: Omit = {}, ): Promise { const downstreamPolicy = options.downstreamPolicy ?? DEFAULT_DOWNSTREAM_POLICY; const match = await this.findRouteById(routeId, downstreamPolicy); const requestedModel = match?.route.modelPattern || `route:${routeId}`; await this.refreshPricingReferenceCostsForMatch(match, requestedModel, { ...options, useChannelSourceModelForCost: true, }); } private explainSelectionFromMatch( match: RouteMatch | null, requestedModel: string, options: ExplainSelectionOptions = {}, ): RouteDecisionExplanation { const excludeChannelIds = options.excludeChannelIds ?? []; const downstreamPolicy = options.downstreamPolicy ?? DEFAULT_DOWNSTREAM_POLICY; if (!match) { return { requestedModel, actualModel: requestedModel, matched: false, summary: ['未匹配到启用的路由'], candidates: [], }; } const requestedByDisplayName = isRouteDisplayNameMatch(requestedModel, match.route.displayName); const bypassSourceModelCheck = (options.bypassSourceModelCheck ?? false) || requestedByDisplayName; const useChannelSourceModelForCost = (options.useChannelSourceModelForCost ?? false) || requestedByDisplayName; const mappedModel = resolveMappedModel(requestedModel, match.route.modelMapping); const routeStrategy = resolveRouteStrategy(match.route); const runtimeModelResolver = requestedByDisplayName ? ((candidate: RouteChannelCandidate) => normalizeChannelSourceModel(candidate.channel.sourceModel) || mappedModel) : mappedModel; const nowIso = new Date().toISOString(); const nowMs = Date.now(); const summary: string[] = [ `命中路由:${match.route.modelPattern}`, routeStrategy === 'round_robin' ? '路由策略:轮询' : (routeStrategy === 'stable_first' ? '路由策略:稳定优先' : '路由策略:按权重随机'), ]; if (requestedByDisplayName) { summary.push(`按显示名命中:${normalizeRouteDisplayName(match.route.displayName)}`); summary.push('显示名仅用于聚合展示,实际转发模型按选中通道来源模型决定'); } const availableByPriority = new Map(); const candidates: RouteDecisionCandidate[] = []; const candidateMap = new Map(); for (const row of match.channels) { const reasonParts = this.getCandidateEligibilityReasons(row, { requestedModel, bypassSourceModelCheck, excludeChannelIds, nowIso, }); const recentlyFailed = routeStrategy !== 'round_robin' ? isChannelRecentlyFailed(row.channel, nowMs) : false; const eligible = reasonParts.length === 0; const candidate: RouteDecisionCandidate = { channelId: row.channel.id, accountId: row.account.id, username: row.account.username || `account-${row.account.id}`, siteName: row.site.name || 'unknown', tokenName: row.token?.name || 'default', priority: row.channel.priority ?? 0, weight: row.channel.weight ?? 10, eligible, recentlyFailed, avoidedByRecentFailure: false, probability: 0, reason: eligible ? '可用' : reasonParts.join('、'), }; candidates.push(candidate); candidateMap.set(candidate.channelId, candidate); if (eligible) { const priority = row.channel.priority ?? 0; if (!availableByPriority.has(priority)) availableByPriority.set(priority, []); availableByPriority.get(priority)!.push(row); } } if (availableByPriority.size === 0) { summary.push('没有可用通道(全部被禁用、站点不可用、冷却或令牌不可用)'); return { requestedModel, actualModel: mappedModel, matched: true, routeId: match.route.id, modelPattern: match.route.modelPattern, summary, candidates, }; } if (routeStrategy === 'round_robin') { const rawOrdered = this.getRoundRobinCandidates(match.channels.filter((row) => { const target = candidateMap.get(row.channel.id); return !!target?.eligible; })); const breakerFiltered = filterSiteRuntimeBrokenCandidatesByModel(rawOrdered, runtimeModelResolver, nowMs); if (breakerFiltered.avoided.length > 0) { for (const item of breakerFiltered.avoided) { const target = candidateMap.get(item.candidate.channel.id); if (!target) continue; target.reason = item.reason; } const breakerSummaryLabel = breakerFiltered.avoided.some((item) => item.reason.includes('模型熔断')) ? '运行时熔断避让' : '站点熔断避让'; summary.push(`${breakerSummaryLabel} ${breakerFiltered.avoided.length}`); } const ordered = breakerFiltered.candidates; let selected: RouteChannelCandidate | null = null; for (let index = 0; index < ordered.length; index += 1) { const target = candidateMap.get(ordered[index].channel.id); if (!target || !target.eligible) continue; target.probability = index === 0 ? 100 : 0; target.reason = index === 0 ? `轮询命中(全局第 1 / ${ordered.length} 位,忽略优先级)` : `轮询排队中(全局第 ${index + 1} / ${ordered.length} 位,忽略优先级)`; if (index === 0) { selected = ordered[index]; } } if (!selected) { summary.push('本次未选出通道'); return { requestedModel, actualModel: mappedModel, matched: true, routeId: match.route.id, modelPattern: match.route.modelPattern, summary, candidates, }; } const selectedChannel = candidateMap.get(selected.channel.id); const selectedLabel = selectedChannel ? `${selectedChannel.username} @ ${selectedChannel.siteName} / ${selectedChannel.tokenName}` : `channel-${selected.channel.id}`; const actualModel = resolveActualModelForSelectedChannel( requestedModel, match.route, mappedModel, selected.channel.sourceModel, ); summary.push(`全局轮询:可用 ${ordered.length},忽略优先级`); summary.push(`最终选择:${selectedLabel}`); if (actualModel !== mappedModel) { summary.push(`实际转发模型:${actualModel}`); } return { requestedModel, actualModel, matched: true, routeId: match.route.id, modelPattern: match.route.modelPattern, selectedChannelId: selected.channel.id, selectedAccountId: selected.account.id, selectedLabel, summary, candidates, }; } const sortedPriorities = Array.from(availableByPriority.keys()).sort((a, b) => a - b); let selected: RouteChannelCandidate | null = null; let selectedPriority = 0; for (const priority of sortedPriorities) { const rawLayer = availableByPriority.get(priority) ?? []; if (rawLayer.length === 0) continue; const breakerFiltered = filterSiteRuntimeBrokenCandidatesByModel(rawLayer, runtimeModelResolver, nowMs); if (breakerFiltered.avoided.length > 0) { for (const item of breakerFiltered.avoided) { const target = candidateMap.get(item.candidate.channel.id); if (!target) continue; target.reason = item.reason; } } const filteredLayer = filterRecentlyFailedCandidates(breakerFiltered.candidates, nowMs); const avoided = breakerFiltered.candidates.filter((row) => !filteredLayer.some((item) => item.channel.id === row.channel.id)); if (avoided.length > 0) { for (const row of avoided) { const target = candidateMap.get(row.channel.id); if (!target) continue; target.avoidedByRecentFailure = true; target.reason = `最近失败,优先避让(${resolveFailureBackoffSec(row.channel.failCount)} 秒窗口)`; } } const weighted = this.calculateWeightedSelection( filteredLayer, useChannelSourceModelForCost ? runtimeModelResolver : mappedModel, downstreamPolicy, nowMs, routeStrategy === 'stable_first' ? 'stable_first' : 'weighted', ); for (const detail of weighted.details) { const target = candidateMap.get(detail.candidate.channel.id); if (!target) continue; target.probability = Number((detail.probability * 100).toFixed(2)); if (target.eligible && !target.avoidedByRecentFailure) { target.reason = detail.reason; } } if (!weighted.selected) continue; selected = weighted.selected; selectedPriority = priority; const layerSummaryParts = [`优先级 P${priority}:可用 ${rawLayer.length}`]; if (breakerFiltered.avoided.length > 0) { const breakerSummaryLabel = breakerFiltered.avoided.some((item) => item.reason.includes('模型熔断')) ? '运行时熔断避让' : '站点熔断避让'; layerSummaryParts.push(`${breakerSummaryLabel} ${breakerFiltered.avoided.length}`); } if (avoided.length > 0) { layerSummaryParts.push(`最近失败避让 ${avoided.length}`); } summary.push(layerSummaryParts.join(',')); break; } if (!selected) { summary.push('本次未选出通道'); return { requestedModel, actualModel: mappedModel, matched: true, routeId: match.route.id, modelPattern: match.route.modelPattern, summary, candidates, }; } const selectedChannel = candidateMap.get(selected.channel.id); const selectedLabel = selectedChannel ? `${selectedChannel.username} @ ${selectedChannel.siteName} / ${selectedChannel.tokenName}` : `channel-${selected.channel.id}`; const actualModel = resolveActualModelForSelectedChannel( requestedModel, match.route, mappedModel, selected.channel.sourceModel, ); summary.push(`最终选择:${selectedLabel}(P${selectedPriority})`); if (actualModel !== mappedModel) { summary.push(`实际转发模型:${actualModel}`); } return { requestedModel, actualModel, matched: true, routeId: match.route.id, modelPattern: match.route.modelPattern, selectedChannelId: selected.channel.id, selectedAccountId: selected.account.id, selectedLabel, summary, candidates, }; } private async refreshPricingReferenceCostsForMatch( match: RouteMatch | null, requestedModel: string, options: PricingReferenceRefreshOptions = {}, ): Promise { if (!match) return; const requestedByDisplayName = isRouteDisplayNameMatch(requestedModel, match.route.displayName); const useChannelSourceModelForCost = (options.useChannelSourceModelForCost ?? false) || requestedByDisplayName; const mappedModel = resolveMappedModel(requestedModel, match.route.modelMapping); const refreshedKeys = options.refreshedKeys ?? new Set(); await Promise.allSettled(match.channels.map(async (candidate) => { const refreshKey = `${candidate.site.id}:${candidate.account.id}`; if (refreshedKeys.has(refreshKey)) return; refreshedKeys.add(refreshKey); const modelName = useChannelSourceModelForCost ? (normalizeChannelSourceModel(candidate.channel.sourceModel) || mappedModel) : mappedModel; if (!modelName) return; await refreshModelPricingCatalog({ site: { id: candidate.site.id, url: candidate.site.url, platform: candidate.site.platform, apiKey: candidate.site.apiKey, }, account: { id: candidate.account.id, accessToken: candidate.account.accessToken, apiToken: candidate.account.apiToken, }, modelName, }); })); } /** * Record success for a channel. */ async recordSuccess(channelId: number, latencyMs: number, cost: number, modelName?: string | null) { await ensureSiteRuntimeHealthStateLoaded(); const row = await db.select() .from(schema.routeChannels) .innerJoin(schema.accounts, eq(schema.routeChannels.accountId, schema.accounts.id)) .where(eq(schema.routeChannels.id, channelId)) .get(); if (!row) return; const ch = row.route_channels; const account = row.accounts; const nowIso = new Date().toISOString(); const nextSuccessCount = (ch.successCount ?? 0) + 1; const nextTotalLatencyMs = (ch.totalLatencyMs ?? 0) + latencyMs; const nextTotalCost = (ch.totalCost ?? 0) + cost; await db.update(schema.routeChannels).set({ successCount: nextSuccessCount, totalLatencyMs: nextTotalLatencyMs, totalCost: nextTotalCost, lastUsedAt: nowIso, cooldownUntil: null, lastFailAt: null, consecutiveFailCount: 0, cooldownLevel: 0, }).where(eq(schema.routeChannels.id, channelId)).run(); patchCachedChannel(channelId, (channel) => { channel.successCount = nextSuccessCount; channel.totalLatencyMs = nextTotalLatencyMs; channel.totalCost = nextTotalCost; channel.lastUsedAt = nowIso; channel.cooldownUntil = null; channel.lastFailAt = null; channel.consecutiveFailCount = 0; channel.cooldownLevel = 0; }); recordSiteRuntimeSuccess(account.siteId, latencyMs, modelName); } /** * Record failure and set cooldown. */ async recordFailure(channelId: number, context: SiteRuntimeFailureContext | string | null = {}) { await ensureSiteRuntimeHealthStateLoaded(); const row = await db.select() .from(schema.routeChannels) .innerJoin(schema.accounts, eq(schema.routeChannels.accountId, schema.accounts.id)) .innerJoin(schema.tokenRoutes, eq(schema.routeChannels.routeId, schema.tokenRoutes.id)) .where(eq(schema.routeChannels.id, channelId)) .get(); if (!row) return; const ch = row.route_channels; const account = row.accounts; const route = row.token_routes; const nowMs = Date.now(); const nowIso = new Date(nowMs).toISOString(); const normalizedContext: SiteRuntimeFailureContext = typeof context === 'string' ? { modelName: context } : (context ?? {}); const shortWindowLimitCooldownUntil = resolveShortWindowLimitCooldown(account, normalizedContext, nowMs); const failCount = shortWindowLimitCooldownUntil ? 0 : ((ch.failCount ?? 0) + 1); const routeStrategy = resolveRouteStrategy(route); const affectedChannelIds = shortWindowLimitCooldownUntil ? await loadCredentialScopedChannelIds(ch, account.id) : [channelId]; let cooldownUntil: string | null = null; let consecutiveFailCount = Math.max(0, ch.consecutiveFailCount ?? 0) + 1; let cooldownLevel = Math.max(0, ch.cooldownLevel ?? 0); if (shortWindowLimitCooldownUntil) { cooldownUntil = shortWindowLimitCooldownUntil; consecutiveFailCount = 0; cooldownLevel = 0; } else if (routeStrategy === 'round_robin') { if (consecutiveFailCount >= ROUND_ROBIN_FAILURE_THRESHOLD) { cooldownLevel = Math.min(cooldownLevel + 1, ROUND_ROBIN_COOLDOWN_LEVELS_SEC.length - 1); const cooldownSec = resolveRoundRobinCooldownSec(cooldownLevel); cooldownUntil = cooldownSec > 0 ? new Date(nowMs + cooldownSec * 1000).toISOString() : null; consecutiveFailCount = 0; } } else { const cooldownSec = resolveFailureBackoffSec(failCount); cooldownUntil = new Date(nowMs + cooldownSec * 1000).toISOString(); consecutiveFailCount = 0; cooldownLevel = 0; } await db.update(schema.routeChannels).set({ failCount, lastFailAt: nowIso, consecutiveFailCount, cooldownLevel, cooldownUntil, }).where(inArray(schema.routeChannels.id, affectedChannelIds)).run(); for (const affectedChannelId of affectedChannelIds) { patchCachedChannel(affectedChannelId, (channel) => { channel.failCount = failCount; channel.lastFailAt = nowIso; channel.cooldownUntil = cooldownUntil; channel.consecutiveFailCount = consecutiveFailCount; channel.cooldownLevel = cooldownLevel; }); } recordSiteRuntimeFailure(account.siteId, normalizedContext, nowMs); } /** * Get all available models (aggregated from all routes). */ async getAvailableModels(): Promise { const routes = await loadEnabledRoutes(); const exposed = buildVisibleEnabledRoutes(routes) .map((route) => getExposedModelNameForRoute(route).trim()) .filter((name) => name.length > 0); return Array.from(new Set(exposed)); } // --- Private methods --- private async selectFromMatch( match: RouteMatch, requestedModel: string, downstreamPolicy: DownstreamRoutingPolicy, excludeChannelIds: number[] = [], recordSelection = true, ): Promise { const mappedModel = resolveMappedModel(requestedModel, match.route.modelMapping); const requestedByDisplayName = isRouteDisplayNameMatch(requestedModel, match.route.displayName); const bypassSourceModelCheck = requestedByDisplayName; const routeStrategy = resolveRouteStrategy(match.route); const runtimeModelResolver = requestedByDisplayName ? ((candidate: RouteChannelCandidate) => normalizeChannelSourceModel(candidate.channel.sourceModel) || mappedModel) : mappedModel; const nowIso = new Date().toISOString(); const nowMs = Date.now(); const available = match.channels.filter((candidate) => ( this.getCandidateEligibilityReasons(candidate, { requestedModel, bypassSourceModelCheck, excludeChannelIds, nowIso, }).length === 0 )); if (available.length === 0) return null; if (routeStrategy === 'round_robin') { const breakerFiltered = filterSiteRuntimeBrokenCandidatesByModel(available, runtimeModelResolver, nowMs); const selected = this.selectRoundRobinCandidate(breakerFiltered.candidates); if (!selected) return null; const tokenValue = this.resolveChannelTokenValue(selected); if (!tokenValue) return null; if (recordSelection) { await this.recordChannelSelection(selected.channel.id); } const actualModel = resolveActualModelForSelectedChannel( requestedModel, match.route, mappedModel, selected.channel.sourceModel, ); return { ...selected, tokenValue, tokenName: selected.token?.name || 'default', actualModel, }; } const layers = new Map(); for (const candidate of available) { const priority = candidate.channel.priority ?? 0; if (!layers.has(priority)) layers.set(priority, []); layers.get(priority)!.push(candidate); } const sortedPriorities = Array.from(layers.keys()).sort((a, b) => a - b); for (const priority of sortedPriorities) { const rawLayer = layers.get(priority) ?? []; const breakerFiltered = filterSiteRuntimeBrokenCandidatesByModel(rawLayer, runtimeModelResolver, nowMs); const candidates = filterRecentlyFailedCandidates(breakerFiltered.candidates, nowMs); const selected = routeStrategy === 'stable_first' ? this.stableFirstSelect( candidates, requestedByDisplayName ? runtimeModelResolver : mappedModel, downstreamPolicy, nowMs, ) : this.weightedRandomSelect( candidates, requestedByDisplayName ? runtimeModelResolver : mappedModel, downstreamPolicy, nowMs, ); if (!selected) continue; const tokenValue = this.resolveChannelTokenValue(selected); if (!tokenValue) continue; if (routeStrategy === 'stable_first' && recordSelection) { await this.recordChannelSelection(selected.channel.id); } const actualModel = resolveActualModelForSelectedChannel( requestedModel, match.route, mappedModel, selected.channel.sourceModel, ); return { ...selected, tokenValue, tokenName: selected.token?.name || 'default', actualModel, }; } return null; } private async selectPreferredFromMatch( match: RouteMatch, requestedModel: string, preferredChannelId: number, excludeChannelIds: number[] = [], recordSelection = true, ): Promise { const mappedModel = resolveMappedModel(requestedModel, match.route.modelMapping); const requestedByDisplayName = isRouteDisplayNameMatch(requestedModel, match.route.displayName); const bypassSourceModelCheck = requestedByDisplayName; const routeStrategy = resolveRouteStrategy(match.route); const runtimeModelResolver = requestedByDisplayName ? ((candidate: RouteChannelCandidate) => normalizeChannelSourceModel(candidate.channel.sourceModel) || mappedModel) : mappedModel; const nowIso = new Date().toISOString(); const nowMs = Date.now(); const available = match.channels.filter((candidate) => ( this.getCandidateEligibilityReasons(candidate, { requestedModel, bypassSourceModelCheck, excludeChannelIds, nowIso, }).length === 0 )); const preferred = available.find((candidate) => candidate.channel.id === preferredChannelId); if (!preferred) return null; const breakerFiltered = filterSiteRuntimeBrokenCandidatesByModel([preferred], runtimeModelResolver, nowMs); if (breakerFiltered.candidates.length <= 0) return null; const selected = breakerFiltered.candidates.find((candidate) => candidate.channel.id === preferredChannelId); if (!selected) return null; if (routeStrategy !== 'round_robin' && isChannelRecentlyFailed(selected.channel, nowMs)) { return null; } const tokenValue = this.resolveChannelTokenValue(selected); if (!tokenValue) return null; if (recordSelection && (routeStrategy === 'round_robin' || routeStrategy === 'stable_first')) { await this.recordChannelSelection(selected.channel.id); } const actualModel = resolveActualModelForSelectedChannel( requestedModel, match.route, mappedModel, selected.channel.sourceModel, ); return { ...selected, tokenValue, tokenName: selected.token?.name || 'default', actualModel, }; } private async findRoute(model: string, downstreamPolicy: DownstreamRoutingPolicy): Promise { let routes = await loadEnabledRoutes(); const supportedPatterns = Array.isArray(downstreamPolicy.supportedModels) ? downstreamPolicy.supportedModels : []; const matchedSupportedPattern = supportedPatterns.some((pattern) => matchesModelPattern(model, pattern)); if (downstreamPolicy.allowedRouteIds.length > 0 && !matchedSupportedPattern) { const allowSet = new Set(downstreamPolicy.allowedRouteIds); routes = routes.filter((route) => allowSet.has(route.id)); } const matchedRoute = routes.find((route) => ( !isExplicitGroupRoute(route) && isExactRouteModelPattern(route.modelPattern) && (route.modelPattern || '').trim() === model )) || routes.find((route) => isExplicitGroupRoute(route) && isRouteDisplayNameMatch(model, route.displayName)) || routes.find((route) => !isExplicitGroupRoute(route) && isRouteDisplayNameMatch(model, route.displayName)) || routes.find((route) => !isExplicitGroupRoute(route) && matchesModelPattern(model, route.modelPattern)); if (!matchedRoute) return null; return await this.loadRouteMatch(matchedRoute); } private async findRouteById(routeId: number, downstreamPolicy: DownstreamRoutingPolicy): Promise { if (downstreamPolicy.allowedRouteIds.length > 0 && !downstreamPolicy.allowedRouteIds.includes(routeId)) { return null; } const route = (await loadEnabledRoutes()).find((item) => item.id === routeId); if (!route) return null; return await this.loadRouteMatch(route); } private async loadRouteMatch(route: RouteRow): Promise { return await loadRouteMatch(route); } private resolveChannelTokenValue(candidate: { channel: typeof schema.routeChannels.$inferSelect; account: typeof schema.accounts.$inferSelect; site?: typeof schema.sites.$inferSelect | null; token: typeof schema.accountTokens.$inferSelect | null; }): string | null { if (candidate.channel.tokenId) { if (!candidate.token) return null; if (!isUsableAccountToken(candidate.token)) return null; const token = candidate.token.token?.trim(); return token ? token : null; } if (getOauthInfoFromAccount(candidate.account)) { const accessToken = candidate.account.accessToken?.trim(); if (accessToken) return accessToken; return null; } const fallback = candidate.account.apiToken?.trim(); if (fallback) return fallback; return null; } private getCandidateEligibilityReasons( candidate: RouteChannelCandidate, options: CandidateEligibilityOptions, ): string[] { const reasonParts: string[] = []; const bypassSourceModelCheck = options.bypassSourceModelCheck ?? false; const excludeChannelIds = options.excludeChannelIds ?? []; const nowIso = options.nowIso ?? new Date().toISOString(); if (!bypassSourceModelCheck && !channelSupportsRequestedModel(candidate.channel.sourceModel, options.requestedModel)) { reasonParts.push(`来源模型不匹配=${candidate.channel.sourceModel || ''}`); } if (!candidate.channel.enabled) reasonParts.push('通道禁用'); if (isExplicitTokenChannel(candidate)) { if (candidate.account.status === 'disabled') { reasonParts.push(`账号状态=${candidate.account.status}`); } } else if (candidate.account.status !== 'active') { reasonParts.push(`账号状态=${candidate.account.status}`); } if (isSiteDisabled(candidate.site.status)) { reasonParts.push(`站点状态=${candidate.site.status || 'disabled'}`); } if (excludeChannelIds.includes(candidate.channel.id)) { reasonParts.push('当前请求已尝试'); } const tokenValue = this.resolveChannelTokenValue(candidate); if (!tokenValue) reasonParts.push('令牌不可用'); if (candidate.channel.cooldownUntil && candidate.channel.cooldownUntil > nowIso) { reasonParts.push('冷却中'); } return reasonParts; } private getRoundRobinCandidates(candidates: RouteChannelCandidate[]): RouteChannelCandidate[] { return [...candidates].sort((left, right) => { const selectionOrder = compareNullableTimeAsc( left.channel.lastSelectedAt || left.channel.lastUsedAt, right.channel.lastSelectedAt || right.channel.lastUsedAt, ); if (selectionOrder !== 0) return selectionOrder; const usedOrder = compareNullableTimeAsc(left.channel.lastUsedAt, right.channel.lastUsedAt); if (usedOrder !== 0) return usedOrder; return (left.channel.id ?? 0) - (right.channel.id ?? 0); }); } private selectRoundRobinCandidate(candidates: RouteChannelCandidate[]): RouteChannelCandidate | null { return this.getRoundRobinCandidates(candidates)[0] ?? null; } private compareStableFirstCandidates(left: RouteChannelCandidate, right: RouteChannelCandidate): number { const selectionOrder = compareNullableTimeAsc( left.channel.lastSelectedAt || left.channel.lastUsedAt, right.channel.lastSelectedAt || right.channel.lastUsedAt, ); if (selectionOrder !== 0) return selectionOrder; const usedOrder = compareNullableTimeAsc(left.channel.lastUsedAt, right.channel.lastUsedAt); if (usedOrder !== 0) return usedOrder; return (left.channel.id ?? 0) - (right.channel.id ?? 0); } private async recordChannelSelection(channelId: number): Promise { const nowIso = new Date().toISOString(); await db.update(schema.routeChannels).set({ lastSelectedAt: nowIso, }).where(eq(schema.routeChannels.id, channelId)).run(); patchCachedChannel(channelId, (channel) => { channel.lastSelectedAt = nowIso; }); } private weightedRandomSelect( candidates: RouteChannelCandidate[], modelName: string | ((candidate: RouteChannelCandidate) => string), downstreamPolicy: DownstreamRoutingPolicy, nowMs = Date.now(), ) { return this.calculateWeightedSelection(candidates, modelName, downstreamPolicy, nowMs, 'weighted').selected; } private stableFirstSelect( candidates: RouteChannelCandidate[], modelName: string | ((candidate: RouteChannelCandidate) => string), downstreamPolicy: DownstreamRoutingPolicy, nowMs = Date.now(), ) { return this.calculateWeightedSelection(candidates, modelName, downstreamPolicy, nowMs, 'stable_first').selected; } private calculateWeightedSelection( candidates: RouteChannelCandidate[], modelName: string | ((candidate: RouteChannelCandidate) => string), downstreamPolicy: DownstreamRoutingPolicy, nowMs = Date.now(), selectionMode: WeightedSelectionMode = 'weighted', ) { if (candidates.length === 0) { return { selected: null as RouteChannelCandidate | null, details: [] as Array<{ candidate: RouteChannelCandidate; probability: number; reason: string }>, }; } if (candidates.length === 1) { return { selected: candidates[0], details: [{ candidate: candidates[0], probability: 1, reason: selectionMode === 'stable_first' ? '稳定优先(唯一可用候选)' : '唯一可用候选', }], }; } const { baseWeightFactor, valueScoreFactor, costWeight, balanceWeight, usageWeight } = config.routingWeights; const resolveModelName = typeof modelName === 'function' ? modelName : (() => modelName); const effectiveCosts = candidates.map((candidate) => resolveEffectiveUnitCost(candidate, resolveModelName(candidate))); const runtimeHealthDetails = candidates.map((candidate) => ( getSiteRuntimeHealthDetails(candidate.site.id, resolveModelName(candidate), nowMs) )); const valueScores = candidates.map((c, i) => { const unitCost = effectiveCosts[i]?.unitCost || 1; const balance = c.account.balance || 0; const totalUsed = (c.channel.successCount ?? 0) + (c.channel.failCount ?? 0); const recentUsage = Math.max(totalUsed, 1); return costWeight * (1 / unitCost) + balanceWeight * balance + usageWeight * (1 / recentUsage); }); const maxVS = Math.max(...valueScores, 0.001); const minVS = Math.min(...valueScores, 0); const range = maxVS - minVS || 1; const normalizedVS = valueScores.map((v) => (v - minVS) / range); const baseContributions = candidates.map((c, i) => { const weight = c.channel.weight ?? 10; return (weight + 10) * (baseWeightFactor + normalizedVS[i] * valueScoreFactor); }); // Avoid over-favoring a site that has many tokens/channels for the same route. // Site-level total contribution remains comparable, then split across its channels. const siteChannelCounts = new Map(); for (const candidate of candidates) { siteChannelCounts.set(candidate.site.id, (siteChannelCounts.get(candidate.site.id) || 0) + 1); } const siteHistoricalHealthMetrics = buildSiteHistoricalHealthMetrics(candidates); const contributions = candidates.map((candidate, i) => { const siteChannels = Math.max(1, siteChannelCounts.get(candidate.site.id) || 1); let contribution = baseContributions[i] / siteChannels; const downstreamSiteMultiplier = downstreamPolicy.siteWeightMultipliers[candidate.site.id] ?? 1; const normalizedDownstreamSiteMultiplier = (Number.isFinite(downstreamSiteMultiplier) && downstreamSiteMultiplier > 0) ? downstreamSiteMultiplier : 1; const siteGlobalWeight = (Number.isFinite(candidate.site.globalWeight) && (candidate.site.globalWeight || 0) > 0) ? (candidate.site.globalWeight as number) : 1; const combinedSiteWeight = siteGlobalWeight * normalizedDownstreamSiteMultiplier; if (combinedSiteWeight > 0 && Number.isFinite(combinedSiteWeight)) { contribution *= combinedSiteWeight; } contribution *= runtimeHealthDetails[i]?.combinedMultiplier ?? 1; contribution *= siteHistoricalHealthMetrics.get(candidate.site.id)?.multiplier ?? 1; // If upstream price is unknown and we are using fallback unit cost, // apply an explicit penalty so raising fallback cost meaningfully lowers probability. if (effectiveCosts[i]?.source === 'fallback') { contribution *= 1 / Math.max(1, effectiveCosts[i]?.unitCost || 1); } return contribution; }); const totalContribution = contributions.reduce((a, b) => a + b, 0); const rankedIndices = candidates.map((_, index) => index) .sort((leftIndex, rightIndex) => { const contributionDiff = contributions[rightIndex] - contributions[leftIndex]; if (Math.abs(contributionDiff) > 1e-9) { return contributionDiff > 0 ? 1 : -1; } return this.compareStableFirstCandidates(candidates[leftIndex], candidates[rightIndex]); }); const rankByIndex = new Map(); rankedIndices.forEach((candidateIndex, rank) => { rankByIndex.set(candidateIndex, rank + 1); }); const details = candidates.map((candidate, i) => { const probability = totalContribution > 0 ? contributions[i] / totalContribution : 0; const weight = candidate.channel.weight ?? 10; const cost = effectiveCosts[i]; const costSourceText = cost?.source === 'observed' ? '实测' : (cost?.source === 'configured' ? '配置' : (cost?.source === 'catalog' ? '目录' : '默认')); const siteChannels = Math.max(1, siteChannelCounts.get(candidate.site.id) || 1); const downstreamSiteMultiplier = downstreamPolicy.siteWeightMultipliers[candidate.site.id] ?? 1; const normalizedDownstreamSiteMultiplier = (Number.isFinite(downstreamSiteMultiplier) && downstreamSiteMultiplier > 0) ? downstreamSiteMultiplier : 1; const siteGlobalWeight = (Number.isFinite(candidate.site.globalWeight) && (candidate.site.globalWeight || 0) > 0) ? (candidate.site.globalWeight as number) : 1; const combinedSiteWeight = siteGlobalWeight * normalizedDownstreamSiteMultiplier; const siteRuntimeDetail = runtimeHealthDetails[i]; const siteHistoricalHealth = siteHistoricalHealthMetrics.get(candidate.site.id); const siteHistoricalMultiplier = siteHistoricalHealth?.multiplier ?? 1; const historicalSuccessRateText = siteHistoricalHealth?.successRate == null ? '—' : `${(siteHistoricalHealth.successRate * 100).toFixed(1)}%`; const historicalLatencyText = siteHistoricalHealth?.avgLatencyMs == null ? '—' : `${siteHistoricalHealth.avgLatencyMs}ms`; const runtimeHealthText = siteRuntimeDetail.modelKey ? `${siteRuntimeDetail.combinedMultiplier.toFixed(2)}(站点=${siteRuntimeDetail.globalMultiplier.toFixed(2)},模型=${siteRuntimeDetail.modelMultiplier.toFixed(2)})` : `${siteRuntimeDetail.globalMultiplier.toFixed(2)}`; const reasonPrefix = selectionMode === 'stable_first' ? `稳定优先(综合评分第 ${rankByIndex.get(i) ?? 1} / ${candidates.length}` : '按权重随机'; return { candidate, probability, reason: selectionMode === 'stable_first' ? `${reasonPrefix},W=${weight},成本=${costSourceText}:${(cost?.unitCost || 1).toFixed(6)},站点权重=${siteGlobalWeight.toFixed(2)}x下游倍率=${normalizedDownstreamSiteMultiplier.toFixed(2)}=${combinedSiteWeight.toFixed(2)},运行时健康=${runtimeHealthText},历史健康=${siteHistoricalMultiplier.toFixed(2)}(成功率=${historicalSuccessRateText},均延迟=${historicalLatencyText},样本=${siteHistoricalHealth?.totalCalls ?? 0}),同站点通道=${siteChannels},评分占比≈${(probability * 100).toFixed(1)}%)` : `按权重随机(W=${weight},成本=${costSourceText}:${(cost?.unitCost || 1).toFixed(6)},站点权重=${siteGlobalWeight.toFixed(2)}x下游倍率=${normalizedDownstreamSiteMultiplier.toFixed(2)}=${combinedSiteWeight.toFixed(2)},运行时健康=${runtimeHealthText},历史健康=${siteHistoricalMultiplier.toFixed(2)}(成功率=${historicalSuccessRateText},均延迟=${historicalLatencyText},样本=${siteHistoricalHealth?.totalCalls ?? 0}),同站点通道=${siteChannels},概率≈${(probability * 100).toFixed(1)}%)`, }; }); let selected = candidates[rankedIndices[0] ?? 0]; if (selectionMode === 'weighted') { let rand = Math.random() * totalContribution; selected = candidates[candidates.length - 1]; for (let i = 0; i < candidates.length; i++) { rand -= contributions[i]; if (rand <= 0) { selected = candidates[i]; break; } } } return { selected, details }; } } export const tokenRouter = new TokenRouter(); export const __tokenRouterTestUtils = { resolveMappedModel, };