forked from boos/BoosAPI
refactor: consolidate single-source routing and protocol helpers
This commit is contained in:
Vendored
+25
@@ -0,0 +1,25 @@
|
||||
export type RouteMode = 'pattern' | 'explicit_group';
|
||||
export type RouteDecisionCandidate = {
|
||||
channelId: number;
|
||||
accountId: number;
|
||||
username: string;
|
||||
siteName: string;
|
||||
tokenName: string;
|
||||
priority: number;
|
||||
weight: number;
|
||||
eligible: boolean;
|
||||
recentlyFailed: boolean;
|
||||
avoidedByRecentFailure: boolean;
|
||||
probability: number;
|
||||
reason: string;
|
||||
};
|
||||
export type RouteDecision = {
|
||||
requestedModel: string;
|
||||
actualModel: string;
|
||||
matched: boolean;
|
||||
selectedChannelId?: number;
|
||||
selectedLabel?: string;
|
||||
summary: string[];
|
||||
candidates: RouteDecisionCandidate[];
|
||||
};
|
||||
export declare function normalizeTokenRouteMode(routeMode: unknown): RouteMode;
|
||||
@@ -0,0 +1,3 @@
|
||||
export function normalizeTokenRouteMode(routeMode) {
|
||||
return routeMode === 'explicit_group' ? 'explicit_group' : 'pattern';
|
||||
}
|
||||
@@ -0,0 +1,12 @@
|
||||
import { describe, expect, it } from 'vitest';
|
||||
|
||||
describe('token route contract', () => {
|
||||
it('normalizes unknown route modes to pattern', async () => {
|
||||
const { normalizeTokenRouteMode } = await import('./tokenRouteContract.js');
|
||||
|
||||
expect(normalizeTokenRouteMode('explicit_group')).toBe('explicit_group');
|
||||
expect(normalizeTokenRouteMode('pattern')).toBe('pattern');
|
||||
expect(normalizeTokenRouteMode('anything-else')).toBe('pattern');
|
||||
expect(normalizeTokenRouteMode(null)).toBe('pattern');
|
||||
});
|
||||
});
|
||||
Vendored
+7
@@ -0,0 +1,7 @@
|
||||
export declare function isTokenRouteRegexPattern(pattern: string): boolean;
|
||||
export declare function isExactTokenRouteModelPattern(pattern: string): boolean;
|
||||
export declare function parseTokenRouteRegexPattern(pattern: string): {
|
||||
regex: RegExp | null;
|
||||
error: string | null;
|
||||
};
|
||||
export declare function matchesTokenRouteModelPattern(model: string, pattern: string): boolean;
|
||||
@@ -0,0 +1,190 @@
|
||||
function readRegexQuantifierLength(pattern, startIndex) {
|
||||
const ch = pattern[startIndex];
|
||||
if (ch === '*' || ch === '+' || ch === '?')
|
||||
return 1;
|
||||
if (ch !== '{')
|
||||
return 0;
|
||||
let index = startIndex + 1;
|
||||
let sawDigit = false;
|
||||
while (index < pattern.length && /\d/.test(pattern[index])) {
|
||||
sawDigit = true;
|
||||
index += 1;
|
||||
}
|
||||
if (!sawDigit)
|
||||
return 0;
|
||||
if (pattern[index] === ',') {
|
||||
index += 1;
|
||||
while (index < pattern.length && /\d/.test(pattern[index])) {
|
||||
index += 1;
|
||||
}
|
||||
}
|
||||
if (pattern[index] !== '}')
|
||||
return 0;
|
||||
return index - startIndex + 1;
|
||||
}
|
||||
function isSafeRegexPatternBody(body) {
|
||||
if (!body || body.length > 256)
|
||||
return false;
|
||||
if (!/^[a-z0-9\s.^$|()[\]{}+*?\\:_/-]+$/i.test(body))
|
||||
return false;
|
||||
if (body.includes('(?=') || body.includes('(?!') || body.includes('(?<=') || body.includes('(?<!') || body.includes('(?<')) {
|
||||
return false;
|
||||
}
|
||||
if (/(^|[^\\])\\[1-9]/.test(body)) {
|
||||
return false;
|
||||
}
|
||||
const groupStack = [];
|
||||
let escaped = false;
|
||||
let inCharClass = false;
|
||||
for (let index = 0; index < body.length; index += 1) {
|
||||
const ch = body[index];
|
||||
if (escaped) {
|
||||
escaped = false;
|
||||
continue;
|
||||
}
|
||||
if (ch === '\\') {
|
||||
escaped = true;
|
||||
continue;
|
||||
}
|
||||
if (inCharClass) {
|
||||
if (ch === ']')
|
||||
inCharClass = false;
|
||||
continue;
|
||||
}
|
||||
if (ch === '[') {
|
||||
inCharClass = true;
|
||||
continue;
|
||||
}
|
||||
if (ch === '(') {
|
||||
if (body[index + 1] === '?') {
|
||||
if (body[index + 2] !== ':')
|
||||
return false;
|
||||
groupStack.push({ hasInnerQuantifier: false, hasAlternation: false });
|
||||
index += 2;
|
||||
continue;
|
||||
}
|
||||
groupStack.push({ hasInnerQuantifier: false, hasAlternation: false });
|
||||
continue;
|
||||
}
|
||||
if (ch === '|') {
|
||||
if (groupStack.length > 0) {
|
||||
groupStack[groupStack.length - 1].hasAlternation = true;
|
||||
}
|
||||
continue;
|
||||
}
|
||||
if (ch === ')') {
|
||||
const group = groupStack.pop();
|
||||
if (!group)
|
||||
return false;
|
||||
const quantifierLength = readRegexQuantifierLength(body, index + 1);
|
||||
if (quantifierLength > 0 && (group.hasInnerQuantifier || group.hasAlternation)) {
|
||||
return false;
|
||||
}
|
||||
const parent = groupStack[groupStack.length - 1];
|
||||
if (parent && (group.hasInnerQuantifier || quantifierLength > 0)) {
|
||||
parent.hasInnerQuantifier = true;
|
||||
}
|
||||
continue;
|
||||
}
|
||||
const quantifierLength = readRegexQuantifierLength(body, index);
|
||||
if (quantifierLength > 0) {
|
||||
if (groupStack.length > 0) {
|
||||
groupStack[groupStack.length - 1].hasInnerQuantifier = true;
|
||||
}
|
||||
index += quantifierLength - 1;
|
||||
}
|
||||
}
|
||||
return !escaped && !inCharClass && groupStack.length === 0;
|
||||
}
|
||||
function globToRegexSource(glob) {
|
||||
let source = '';
|
||||
for (let i = 0; i < glob.length; i += 1) {
|
||||
const ch = glob[i];
|
||||
if (ch === '*') {
|
||||
source += '.*';
|
||||
continue;
|
||||
}
|
||||
if (ch === '?') {
|
||||
source += '.';
|
||||
continue;
|
||||
}
|
||||
if (ch === '[') {
|
||||
const closeIndex = glob.indexOf(']', i + 1);
|
||||
if (closeIndex > i + 1) {
|
||||
source += glob.slice(i, closeIndex + 1);
|
||||
i = closeIndex;
|
||||
continue;
|
||||
}
|
||||
source += '\\[';
|
||||
continue;
|
||||
}
|
||||
source += ch.replace(/[\\^$+?.()|{}]/g, '\\$&');
|
||||
}
|
||||
return source;
|
||||
}
|
||||
const compiledGlobCache = new Map();
|
||||
const matchCache = new Map();
|
||||
const MATCH_CACHE_LIMIT = 4000;
|
||||
function matchesGlobPattern(model, pattern) {
|
||||
let re = compiledGlobCache.get(pattern);
|
||||
if (re === undefined) {
|
||||
try {
|
||||
re = new RegExp(`^${globToRegexSource(pattern)}$`);
|
||||
}
|
||||
catch {
|
||||
re = null;
|
||||
}
|
||||
compiledGlobCache.set(pattern, re);
|
||||
}
|
||||
return re ? re.test(model) : false;
|
||||
}
|
||||
export function isTokenRouteRegexPattern(pattern) {
|
||||
return pattern.trim().toLowerCase().startsWith('re:');
|
||||
}
|
||||
export function isExactTokenRouteModelPattern(pattern) {
|
||||
const normalized = pattern.trim();
|
||||
if (!normalized)
|
||||
return false;
|
||||
if (isTokenRouteRegexPattern(normalized))
|
||||
return false;
|
||||
return !/[\*\?]/.test(normalized);
|
||||
}
|
||||
export function parseTokenRouteRegexPattern(pattern) {
|
||||
if (!isTokenRouteRegexPattern(pattern))
|
||||
return { regex: null, error: null };
|
||||
const body = pattern.trim().slice(3).trim();
|
||||
if (!body)
|
||||
return { regex: null, error: 're: 后缺少正则表达式' };
|
||||
if (!isSafeRegexPatternBody(body)) {
|
||||
return { regex: null, error: '出于安全原因不支持该正则表达式' };
|
||||
}
|
||||
try {
|
||||
return { regex: new RegExp(body), error: null };
|
||||
}
|
||||
catch (error) {
|
||||
return { regex: null, error: error?.message || '无效正则' };
|
||||
}
|
||||
}
|
||||
export function matchesTokenRouteModelPattern(model, pattern) {
|
||||
const normalized = (pattern || '').trim();
|
||||
if (!normalized)
|
||||
return false;
|
||||
if (normalized === model)
|
||||
return true;
|
||||
const cacheKey = `${model}\0${normalized}`;
|
||||
const cached = matchCache.get(cacheKey);
|
||||
if (cached !== undefined)
|
||||
return cached;
|
||||
let result;
|
||||
if (isTokenRouteRegexPattern(normalized)) {
|
||||
const parsed = parseTokenRouteRegexPattern(normalized);
|
||||
result = !!parsed.regex && parsed.regex.test(model);
|
||||
}
|
||||
else {
|
||||
result = matchesGlobPattern(model, normalized);
|
||||
}
|
||||
if (matchCache.size >= MATCH_CACHE_LIMIT)
|
||||
matchCache.clear();
|
||||
matchCache.set(cacheKey, result);
|
||||
return result;
|
||||
}
|
||||
@@ -0,0 +1,26 @@
|
||||
import { describe, expect, it } from 'vitest';
|
||||
|
||||
describe('token route pattern helpers', () => {
|
||||
it('treats bracket-prefixed literal model names as exact patterns', async () => {
|
||||
const { isExactTokenRouteModelPattern } = await import('./tokenRoutePatterns.js');
|
||||
expect(isExactTokenRouteModelPattern('[NV]deepseek-v3.1-terminus')).toBe(true);
|
||||
});
|
||||
|
||||
it('rejects unsafe nested-quantifier regex patterns', async () => {
|
||||
const {
|
||||
matchesTokenRouteModelPattern,
|
||||
parseTokenRouteRegexPattern,
|
||||
} = await import('./tokenRoutePatterns.js');
|
||||
|
||||
expect(parseTokenRouteRegexPattern('re:^(a+)+$').regex).toBeNull();
|
||||
expect(matchesTokenRouteModelPattern('aaaa', 're:^(a+)+$')).toBe(false);
|
||||
});
|
||||
|
||||
it('supports exact, glob, and safe regex route matches', async () => {
|
||||
const { matchesTokenRouteModelPattern } = await import('./tokenRoutePatterns.js');
|
||||
|
||||
expect(matchesTokenRouteModelPattern('gpt-4o-mini', 'gpt-4o-mini')).toBe(true);
|
||||
expect(matchesTokenRouteModelPattern('claude-sonnet-4-6', 'claude-*')).toBe(true);
|
||||
expect(matchesTokenRouteModelPattern('claude-sonnet-4-6', 're:^claude-(opus|sonnet)-4-6$')).toBe(true);
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user