1
0
forked from boos/BoosAPI

Merge remote-tracking branch 'upstream/main' into feat/site-observability-proxy-logs

This commit is contained in:
root
2026-03-11 11:44:29 +00:00
20 changed files with 3401 additions and 2432 deletions
@@ -0,0 +1,64 @@
export type AccountTokenSchemaDialect = 'sqlite' | 'mysql' | 'postgres';
export interface AccountTokenSchemaInspector {
dialect: AccountTokenSchemaDialect;
tableExists(table: string): Promise<boolean>;
columnExists(table: string, column: string): Promise<boolean>;
execute(sqlText: string): Promise<void>;
}
type AccountTokenColumnCompatibilitySpec = {
table: 'account_tokens';
column: string;
addSql: Record<AccountTokenSchemaDialect, string>;
};
const ACCOUNT_TOKEN_COLUMN_COMPATIBILITY_SPECS: AccountTokenColumnCompatibilitySpec[] = [
{
table: 'account_tokens',
column: 'token_group',
addSql: {
sqlite: 'ALTER TABLE account_tokens ADD COLUMN token_group text;',
mysql: 'ALTER TABLE `account_tokens` ADD COLUMN `token_group` TEXT NULL',
postgres: 'ALTER TABLE "account_tokens" ADD COLUMN "token_group" TEXT',
},
},
];
function normalizeSchemaErrorMessage(error: unknown): string {
if (typeof error === 'object' && error && 'message' in error) {
return String((error as { message?: unknown }).message || '');
}
return String(error || '');
}
function isDuplicateColumnError(error: unknown): boolean {
const lowered = normalizeSchemaErrorMessage(error).toLowerCase();
return lowered.includes('duplicate column')
|| lowered.includes('already exists')
|| lowered.includes('duplicate column name');
}
async function executeAddColumn(inspector: AccountTokenSchemaInspector, sqlText: string): Promise<void> {
try {
await inspector.execute(sqlText);
} catch (error) {
if (!isDuplicateColumnError(error)) {
throw error;
}
}
}
export async function ensureAccountTokenSchemaCompatibility(inspector: AccountTokenSchemaInspector): Promise<void> {
for (const spec of ACCOUNT_TOKEN_COLUMN_COMPATIBILITY_SPECS) {
const hasTable = await inspector.tableExists(spec.table);
if (!hasTable) {
continue;
}
const hasColumn = await inspector.columnExists(spec.table, spec.column);
if (!hasColumn) {
await executeAddColumn(inspector, spec.addSql[inspector.dialect]);
}
}
}
+2
View File
@@ -6,6 +6,7 @@ import { dirname, resolve } from 'node:path';
import { ensureSiteSchemaCompatibility, type SiteSchemaInspector } from './siteSchemaCompatibility.js';
import { ensureRouteGroupingSchemaCompatibility } from './routeGroupingSchemaCompatibility.js';
import { ensureProxyFileSchemaCompatibility } from './proxyFileSchemaCompatibility.js';
import { ensureAccountTokenSchemaCompatibility } from './accountTokenSchemaCompatibility.js';
export type RuntimeSchemaDialect = 'sqlite' | 'mysql' | 'postgres';
@@ -241,6 +242,7 @@ export async function ensureRuntimeDatabaseSchema(client: RuntimeSchemaClient):
await ensureSiteSchemaCompatibility(createSiteSchemaInspector(client));
await ensureRouteGroupingSchemaCompatibility(createSiteSchemaInspector(client));
await ensureProxyFileSchemaCompatibility(createSiteSchemaInspector(client));
await ensureAccountTokenSchemaCompatibility(createSiteSchemaInspector(client));
}
export async function bootstrapRuntimeDatabaseSchema(input: RuntimeSchemaConnectionInput): Promise<void> {
+469 -314
View File
@@ -1,15 +1,15 @@
import { FastifyInstance } from 'fastify';
import { and, eq, inArray } from 'drizzle-orm';
import { db, schema } from '../../db/index.js';
import { rebuildTokenRoutesFromAvailability, refreshModelsAndRebuildRoutes } from '../../services/modelService.js';
import { invalidateTokenRouterCache, matchesModelPattern, tokenRouter } from '../../services/tokenRouter.js';
import { startBackgroundTask } from '../../services/backgroundTaskService.js';
import {
clearRouteDecisionSnapshot,
clearRouteDecisionSnapshots,
parseRouteDecisionSnapshot,
saveRouteDecisionSnapshots,
} from '../../services/routeDecisionSnapshotStore.js';
import { and, eq, inArray } from 'drizzle-orm';
import { db, schema } from '../../db/index.js';
import { rebuildTokenRoutesFromAvailability, refreshModelsAndRebuildRoutes } from '../../services/modelService.js';
import { invalidateTokenRouterCache, matchesModelPattern, tokenRouter } from '../../services/tokenRouter.js';
import { startBackgroundTask } from '../../services/backgroundTaskService.js';
import {
clearRouteDecisionSnapshot,
clearRouteDecisionSnapshots,
parseRouteDecisionSnapshot,
saveRouteDecisionSnapshots,
} from '../../services/routeDecisionSnapshotStore.js';
function isExactModelPattern(modelPattern: string): boolean {
const normalized = modelPattern.trim();
@@ -127,9 +127,9 @@ async function getMatchedExactRouteChannelCandidates(modelPattern: string): Prom
})).filter((candidate) => candidate.sourceModel.length > 0);
}
async function populateRouteChannelsByModelPattern(routeId: number, modelPattern: string): Promise<number> {
const routeCandidates = await getMatchedExactRouteChannelCandidates(modelPattern);
const availabilityCandidates = (await getPatternTokenCandidates(modelPattern)).map((candidate) => ({
async function populateRouteChannelsByModelPattern(routeId: number, modelPattern: string): Promise<number> {
const routeCandidates = await getMatchedExactRouteChannelCandidates(modelPattern);
const availabilityCandidates = (await getPatternTokenCandidates(modelPattern)).map((candidate) => ({
tokenId: candidate.tokenId,
accountId: candidate.accountId,
sourceModel: candidate.sourceModel,
@@ -171,59 +171,59 @@ async function populateRouteChannelsByModelPattern(routeId: number, modelPattern
existingPairs.add(pairKey);
created += 1;
}
return created;
}
async function rebuildAutomaticRouteChannelsByModelPattern(routeId: number, modelPattern: string): Promise<{
removedChannels: number;
createdChannels: number;
}> {
const removableChannels = await db.select().from(schema.routeChannels)
.where(
and(
eq(schema.routeChannels.routeId, routeId),
eq(schema.routeChannels.manualOverride, false),
),
)
.all();
for (const channel of removableChannels) {
await db.delete(schema.routeChannels).where(eq(schema.routeChannels.id, channel.id)).run();
}
const createdChannels = await populateRouteChannelsByModelPattern(routeId, modelPattern);
return {
removedChannels: removableChannels.length,
createdChannels,
};
}
return created;
}
async function rebuildAutomaticRouteChannelsByModelPattern(routeId: number, modelPattern: string): Promise<{
removedChannels: number;
createdChannels: number;
}> {
const removableChannels = await db.select().from(schema.routeChannels)
.where(
and(
eq(schema.routeChannels.routeId, routeId),
eq(schema.routeChannels.manualOverride, false),
),
)
.all();
for (const channel of removableChannels) {
await db.delete(schema.routeChannels).where(eq(schema.routeChannels.id, channel.id)).run();
}
const createdChannels = await populateRouteChannelsByModelPattern(routeId, modelPattern);
return {
removedChannels: removableChannels.length,
createdChannels,
};
}
type BatchChannelPriorityUpdate = {
id: number;
priority: number;
};
type BatchRouteDecisionModels = {
models: string[];
refreshPricingCatalog?: boolean;
persistSnapshots?: boolean;
};
type BatchRouteDecisionRouteModels = {
items: Array<{
routeId: number;
model: string;
}>;
refreshPricingCatalog?: boolean;
persistSnapshots?: boolean;
};
type BatchRouteWideDecisionRouteIds = {
routeIds: number[];
refreshPricingCatalog?: boolean;
persistSnapshots?: boolean;
};
type BatchRouteDecisionModels = {
models: string[];
refreshPricingCatalog?: boolean;
persistSnapshots?: boolean;
};
type BatchRouteDecisionRouteModels = {
items: Array<{
routeId: number;
model: string;
}>;
refreshPricingCatalog?: boolean;
persistSnapshots?: boolean;
};
type BatchRouteWideDecisionRouteIds = {
routeIds: number[];
refreshPricingCatalog?: boolean;
persistSnapshots?: boolean;
};
function parseBatchChannelUpdates(input: unknown): { ok: true; updates: BatchChannelPriorityUpdate[] } | { ok: false; message: string } {
if (!input || typeof input !== 'object') {
@@ -264,9 +264,9 @@ function parseBatchChannelUpdates(input: unknown): { ok: true; updates: BatchCha
return { ok: true, updates: normalized };
}
function parseBatchRouteDecisionModels(
input: unknown,
): { ok: true; models: string[]; refreshPricingCatalog: boolean; persistSnapshots: boolean } | { ok: false; message: string } {
function parseBatchRouteDecisionModels(
input: unknown,
): { ok: true; models: string[]; refreshPricingCatalog: boolean; persistSnapshots: boolean } | { ok: false; message: string } {
if (!input || typeof input !== 'object') {
return { ok: false, message: '请求体必须是对象' };
}
@@ -291,17 +291,17 @@ function parseBatchRouteDecisionModels(
return { ok: false, message: 'models 中没有有效模型名称' };
}
return {
ok: true,
models: normalized,
refreshPricingCatalog: (input as { refreshPricingCatalog?: unknown }).refreshPricingCatalog === true,
persistSnapshots: (input as { persistSnapshots?: unknown }).persistSnapshots === true,
};
}
function parseBatchRouteDecisionRouteModels(
input: unknown,
): { ok: true; items: Array<{ routeId: number; model: string }>; refreshPricingCatalog: boolean; persistSnapshots: boolean } | { ok: false; message: string } {
return {
ok: true,
models: normalized,
refreshPricingCatalog: (input as { refreshPricingCatalog?: unknown }).refreshPricingCatalog === true,
persistSnapshots: (input as { persistSnapshots?: unknown }).persistSnapshots === true,
};
}
function parseBatchRouteDecisionRouteModels(
input: unknown,
): { ok: true; items: Array<{ routeId: number; model: string }>; refreshPricingCatalog: boolean; persistSnapshots: boolean } | { ok: false; message: string } {
if (!input || typeof input !== 'object') {
return { ok: false, message: '请求体必须是对象' };
}
@@ -335,17 +335,17 @@ function parseBatchRouteDecisionRouteModels(
return { ok: false, message: 'items 中没有有效 routeId/model' };
}
return {
ok: true,
items: normalized,
refreshPricingCatalog: (input as { refreshPricingCatalog?: unknown }).refreshPricingCatalog === true,
persistSnapshots: (input as { persistSnapshots?: unknown }).persistSnapshots === true,
};
}
function parseBatchRouteWideDecisionRouteIds(
input: unknown,
): { ok: true; routeIds: number[]; refreshPricingCatalog: boolean; persistSnapshots: boolean } | { ok: false; message: string } {
return {
ok: true,
items: normalized,
refreshPricingCatalog: (input as { refreshPricingCatalog?: unknown }).refreshPricingCatalog === true,
persistSnapshots: (input as { persistSnapshots?: unknown }).persistSnapshots === true,
};
}
function parseBatchRouteWideDecisionRouteIds(
input: unknown,
): { ok: true; routeIds: number[]; refreshPricingCatalog: boolean; persistSnapshots: boolean } | { ok: false; message: string } {
if (!input || typeof input !== 'object') {
return { ok: false, message: '请求体必须是对象' };
}
@@ -370,56 +370,211 @@ function parseBatchRouteWideDecisionRouteIds(
return { ok: false, message: 'routeIds 中没有有效 routeId' };
}
return {
ok: true,
routeIds: normalized,
refreshPricingCatalog: (input as { refreshPricingCatalog?: unknown }).refreshPricingCatalog === true,
persistSnapshots: (input as { persistSnapshots?: unknown }).persistSnapshots === true,
};
}
return {
ok: true,
routeIds: normalized,
refreshPricingCatalog: (input as { refreshPricingCatalog?: unknown }).refreshPricingCatalog === true,
persistSnapshots: (input as { persistSnapshots?: unknown }).persistSnapshots === true,
};
}
async function fetchChannelsForRoutes(routeIds: number[]): Promise<Map<number, any[]>> {
if (routeIds.length === 0) return new Map();
const channelRows = 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, routeIds))
.all();
const channelsByRoute = new Map<number, any[]>();
for (const row of channelRows) {
const routeId = row.route_channels.routeId;
if (!channelsByRoute.has(routeId)) channelsByRoute.set(routeId, []);
channelsByRoute.get(routeId)!.push({
...row.route_channels,
account: row.accounts,
site: row.sites,
token: row.account_tokens
? {
id: row.account_tokens.id,
name: row.account_tokens.name,
accountId: row.account_tokens.accountId,
enabled: row.account_tokens.enabled,
isDefault: row.account_tokens.isDefault,
}
: null,
});
}
return channelsByRoute;
}
export async function tokensRoutes(app: FastifyInstance) {
// List all routes
app.get('/api/routes', async () => {
const routes = await db.select().from(schema.tokenRoutes).all();
if (routes.length === 0) return [];
// List routes with basic info only (lightweight for selectors)
app.get('/api/routes/lite', async () => {
return await db.select({
id: schema.tokenRoutes.id,
modelPattern: schema.tokenRoutes.modelPattern,
displayName: schema.tokenRoutes.displayName,
displayIcon: schema.tokenRoutes.displayIcon,
enabled: schema.tokenRoutes.enabled,
}).from(schema.tokenRoutes).all();
});
// Route summary (no channel details) for first-screen rendering
app.get('/api/routes/summary', async () => {
const routes = await db.select().from(schema.tokenRoutes).all();
if (routes.length === 0) return [];
const routeIds = routes.map((route) => route.id);
const channelRows = await db.select().from(schema.routeChannels)
// Aggregate channel counts and site names per route
const channelRows = await db.select({
routeId: schema.routeChannels.routeId,
enabled: schema.routeChannels.enabled,
siteName: schema.sites.name,
}).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, routeIds))
.all();
const channelsByRoute = new Map<number, any[]>();
const aggByRoute = new Map<number, { channelCount: number; enabledChannelCount: number; siteNames: Set<string> }>();
for (const row of channelRows) {
const routeId = row.route_channels.routeId;
if (!channelsByRoute.has(routeId)) channelsByRoute.set(routeId, []);
channelsByRoute.get(routeId)!.push({
...row.route_channels,
account: row.accounts,
site: row.sites,
token: row.account_tokens
? {
id: row.account_tokens.id,
name: row.account_tokens.name,
accountId: row.account_tokens.accountId,
enabled: row.account_tokens.enabled,
isDefault: row.account_tokens.isDefault,
}
: null,
});
let agg = aggByRoute.get(row.routeId);
if (!agg) {
agg = { channelCount: 0, enabledChannelCount: 0, siteNames: new Set() };
aggByRoute.set(row.routeId, agg);
}
agg.channelCount += 1;
if (row.enabled) agg.enabledChannelCount += 1;
if (row.siteName) agg.siteNames.add(row.siteName);
}
return routes.map((route) => ({
...route,
decisionSnapshot: parseRouteDecisionSnapshot(route.decisionSnapshot),
decisionRefreshedAt: route.decisionRefreshedAt ?? null,
channels: channelsByRoute.get(route.id) || [],
}));
});
return routes.map((route) => {
const agg = aggByRoute.get(route.id);
return {
id: route.id,
modelPattern: route.modelPattern,
displayName: route.displayName ?? null,
displayIcon: route.displayIcon ?? null,
modelMapping: route.modelMapping ?? null,
enabled: route.enabled,
channelCount: agg?.channelCount ?? 0,
enabledChannelCount: agg?.enabledChannelCount ?? 0,
siteNames: agg ? Array.from(agg.siteNames) : [],
decisionSnapshot: parseRouteDecisionSnapshot(route.decisionSnapshot),
decisionRefreshedAt: route.decisionRefreshedAt ?? null,
};
});
});
// Get channels for a single route (on-demand loading)
app.get<{ Params: { id: string } }>('/api/routes/:id/channels', async (request, reply) => {
const routeId = parseInt(request.params.id, 10);
const route = await db.select().from(schema.tokenRoutes).where(eq(schema.tokenRoutes.id, routeId)).get();
if (!route) {
return reply.code(404).send({ success: false, message: '路由不存在' });
}
const channelsByRoute = await fetchChannelsForRoutes([routeId]);
return channelsByRoute.get(routeId) || [];
});
// Batch add channels to a route
app.post<{ Params: { id: string }; Body: { channels: Array<{ accountId: number; tokenId?: number; sourceModel?: string }> } }>('/api/routes/:id/channels/batch', async (request, reply) => {
const routeId = parseInt(request.params.id, 10);
const body = request.body;
const route = await db.select().from(schema.tokenRoutes).where(eq(schema.tokenRoutes.id, routeId)).get();
if (!route) {
return reply.code(404).send({ success: false, message: '路由不存在' });
}
if (!body?.channels || !Array.isArray(body.channels) || body.channels.length === 0) {
return reply.code(400).send({ success: false, message: 'channels 必须是非空数组' });
}
const existingChannels = await db.select().from(schema.routeChannels)
.where(eq(schema.routeChannels.routeId, routeId))
.all();
const existingPairs = new Set<string>(
existingChannels.map((channel) => {
const tokenId = typeof channel.tokenId === 'number' && Number.isFinite(channel.tokenId) ? channel.tokenId : 0;
const sourceModel = (channel.sourceModel || '').trim().toLowerCase();
return `${channel.accountId}::${tokenId}::${sourceModel}`;
}),
);
let created = 0;
let skipped = 0;
const errors: string[] = [];
for (const item of body.channels) {
if (!item?.accountId || typeof item.accountId !== 'number') {
errors.push('无效的 accountId');
continue;
}
const sourceModel = typeof item.sourceModel === 'string'
? item.sourceModel.trim()
: (isExactModelPattern(route.modelPattern) ? route.modelPattern.trim() : '');
const effectiveTokenId = item.tokenId ?? await getDefaultTokenId(item.accountId);
if (item.tokenId && !await checkTokenBelongsToAccount(item.tokenId, item.accountId)) {
errors.push(`令牌 ${item.tokenId} 不属于账号 ${item.accountId}`);
continue;
}
const tokenIdForKey = typeof effectiveTokenId === 'number' && Number.isFinite(effectiveTokenId) ? effectiveTokenId : 0;
const pairKey = `${item.accountId}::${tokenIdForKey}::${sourceModel.toLowerCase()}`;
if (existingPairs.has(pairKey)) {
skipped += 1;
continue;
}
try {
await db.insert(schema.routeChannels).values({
routeId,
accountId: item.accountId,
tokenId: effectiveTokenId,
sourceModel: sourceModel || null,
priority: 0,
weight: 10,
manualOverride: true,
}).run();
existingPairs.add(pairKey);
created += 1;
} catch (e: any) {
errors.push(e.message || `添加通道失败: accountId=${item.accountId}`);
}
}
if (created > 0) {
await clearRouteDecisionSnapshot(routeId);
invalidateTokenRouterCache();
}
return { success: true, created, skipped, errors };
});
// List all routes
app.get('/api/routes', async () => {
const routes = await db.select().from(schema.tokenRoutes).all();
if (routes.length === 0) return [];
const routeIds = routes.map((route) => route.id);
const channelsByRoute = await fetchChannelsForRoutes(routeIds);
return routes.map((route) => ({
...route,
decisionSnapshot: parseRouteDecisionSnapshot(route.decisionSnapshot),
decisionRefreshedAt: route.decisionRefreshedAt ?? null,
channels: channelsByRoute.get(route.id) || [],
}));
});
app.get<{ Querystring: { model?: string } }>('/api/routes/decision', async (request, reply) => {
const model = (request.query.model || '').trim();
@@ -427,157 +582,157 @@ export async function tokensRoutes(app: FastifyInstance) {
return reply.code(400).send({ success: false, message: 'model 不能为空' });
}
const decision = await tokenRouter.explainSelection(model);
const decision = await tokenRouter.explainSelection(model);
return { success: true, decision };
});
app.post<{ Body: BatchRouteDecisionModels }>('/api/routes/decision/batch', async (request, reply) => {
const parsed = parseBatchRouteDecisionModels(request.body);
if (!parsed.ok) {
return reply.code(400).send({ success: false, message: parsed.message });
}
const decisions: Record<string, Awaited<ReturnType<typeof tokenRouter.explainSelection>>> = {};
const routes = parsed.persistSnapshots
? await db.select({
id: schema.tokenRoutes.id,
modelPattern: schema.tokenRoutes.modelPattern,
}).from(schema.tokenRoutes).all()
: [];
const refreshedKeys = parsed.refreshPricingCatalog ? new Set<string>() : undefined;
for (const model of parsed.models) {
if (parsed.refreshPricingCatalog) {
await tokenRouter.refreshPricingReferenceCosts(model, { refreshedKeys });
}
decisions[model] = await tokenRouter.explainSelection(model);
}
if (parsed.persistSnapshots) {
const snapshotWrites: Array<{ routeId: number; snapshot: unknown }> = [];
for (const model of parsed.models) {
const decision = decisions[model];
for (const route of routes) {
if (!isExactModelPattern(route.modelPattern)) continue;
if (!matchesModelPattern(model, route.modelPattern)) continue;
snapshotWrites.push({ routeId: route.id, snapshot: decision });
}
}
await saveRouteDecisionSnapshots(snapshotWrites);
}
return { success: true, decisions };
});
app.post<{ Body: BatchRouteDecisionModels }>('/api/routes/decision/batch', async (request, reply) => {
const parsed = parseBatchRouteDecisionModels(request.body);
if (!parsed.ok) {
return reply.code(400).send({ success: false, message: parsed.message });
}
app.post<{ Body: BatchRouteDecisionRouteModels }>('/api/routes/decision/by-route/batch', async (request, reply) => {
const parsed = parseBatchRouteDecisionRouteModels(request.body);
if (!parsed.ok) {
return reply.code(400).send({ success: false, message: parsed.message });
}
const decisions: Record<string, Record<string, Awaited<ReturnType<typeof tokenRouter.explainSelectionForRoute>>>> = {};
const refreshedKeys = parsed.refreshPricingCatalog ? new Set<string>() : undefined;
for (const item of parsed.items) {
const routeKey = String(item.routeId);
if (!decisions[routeKey]) decisions[routeKey] = {};
if (parsed.refreshPricingCatalog) {
await tokenRouter.refreshPricingReferenceCostsForRoute(item.routeId, item.model, { refreshedKeys });
}
decisions[routeKey][item.model] = await tokenRouter.explainSelectionForRoute(item.routeId, item.model);
}
if (parsed.persistSnapshots) {
await saveRouteDecisionSnapshots(parsed.items.map((item) => ({
routeId: item.routeId,
snapshot: decisions[String(item.routeId)]?.[item.model] ?? null,
})));
}
return { success: true, decisions };
});
const decisions: Record<string, Awaited<ReturnType<typeof tokenRouter.explainSelection>>> = {};
const routes = parsed.persistSnapshots
? await db.select({
id: schema.tokenRoutes.id,
modelPattern: schema.tokenRoutes.modelPattern,
}).from(schema.tokenRoutes).all()
: [];
const refreshedKeys = parsed.refreshPricingCatalog ? new Set<string>() : undefined;
for (const model of parsed.models) {
if (parsed.refreshPricingCatalog) {
await tokenRouter.refreshPricingReferenceCosts(model, { refreshedKeys });
}
decisions[model] = await tokenRouter.explainSelection(model);
}
app.post<{ Body: BatchRouteWideDecisionRouteIds }>('/api/routes/decision/route-wide/batch', async (request, reply) => {
const parsed = parseBatchRouteWideDecisionRouteIds(request.body);
if (!parsed.ok) {
return reply.code(400).send({ success: false, message: parsed.message });
}
const decisions: Record<string, Awaited<ReturnType<typeof tokenRouter.explainSelectionRouteWide>>> = {};
const refreshedKeys = parsed.refreshPricingCatalog ? new Set<string>() : undefined;
for (const routeId of parsed.routeIds) {
if (parsed.refreshPricingCatalog) {
await tokenRouter.refreshRouteWidePricingReferenceCosts(routeId, { refreshedKeys });
}
decisions[String(routeId)] = await tokenRouter.explainSelectionRouteWide(routeId);
}
if (parsed.persistSnapshots) {
await saveRouteDecisionSnapshots(parsed.routeIds.map((routeId) => ({
routeId,
snapshot: decisions[String(routeId)] ?? null,
})));
}
return { success: true, decisions };
});
if (parsed.persistSnapshots) {
const snapshotWrites: Array<{ routeId: number; snapshot: unknown }> = [];
for (const model of parsed.models) {
const decision = decisions[model];
for (const route of routes) {
if (!isExactModelPattern(route.modelPattern)) continue;
if (!matchesModelPattern(model, route.modelPattern)) continue;
snapshotWrites.push({ routeId: route.id, snapshot: decision });
}
}
await saveRouteDecisionSnapshots(snapshotWrites);
}
return { success: true, decisions };
});
app.post<{ Body: BatchRouteDecisionRouteModels }>('/api/routes/decision/by-route/batch', async (request, reply) => {
const parsed = parseBatchRouteDecisionRouteModels(request.body);
if (!parsed.ok) {
return reply.code(400).send({ success: false, message: parsed.message });
}
const decisions: Record<string, Record<string, Awaited<ReturnType<typeof tokenRouter.explainSelectionForRoute>>>> = {};
const refreshedKeys = parsed.refreshPricingCatalog ? new Set<string>() : undefined;
for (const item of parsed.items) {
const routeKey = String(item.routeId);
if (!decisions[routeKey]) decisions[routeKey] = {};
if (parsed.refreshPricingCatalog) {
await tokenRouter.refreshPricingReferenceCostsForRoute(item.routeId, item.model, { refreshedKeys });
}
decisions[routeKey][item.model] = await tokenRouter.explainSelectionForRoute(item.routeId, item.model);
}
if (parsed.persistSnapshots) {
await saveRouteDecisionSnapshots(parsed.items.map((item) => ({
routeId: item.routeId,
snapshot: decisions[String(item.routeId)]?.[item.model] ?? null,
})));
}
return { success: true, decisions };
});
app.post<{ Body: BatchRouteWideDecisionRouteIds }>('/api/routes/decision/route-wide/batch', async (request, reply) => {
const parsed = parseBatchRouteWideDecisionRouteIds(request.body);
if (!parsed.ok) {
return reply.code(400).send({ success: false, message: parsed.message });
}
const decisions: Record<string, Awaited<ReturnType<typeof tokenRouter.explainSelectionRouteWide>>> = {};
const refreshedKeys = parsed.refreshPricingCatalog ? new Set<string>() : undefined;
for (const routeId of parsed.routeIds) {
if (parsed.refreshPricingCatalog) {
await tokenRouter.refreshRouteWidePricingReferenceCosts(routeId, { refreshedKeys });
}
decisions[String(routeId)] = await tokenRouter.explainSelectionRouteWide(routeId);
}
if (parsed.persistSnapshots) {
await saveRouteDecisionSnapshots(parsed.routeIds.map((routeId) => ({
routeId,
snapshot: decisions[String(routeId)] ?? null,
})));
}
return { success: true, decisions };
});
// Create a route
app.post<{ Body: { modelPattern: string; displayName?: string; displayIcon?: string; modelMapping?: string; enabled?: boolean } }>('/api/routes', async (request) => {
const body = request.body;
const insertedRoute = await db.insert(schema.tokenRoutes).values({
modelPattern: body.modelPattern,
displayName: body.displayName,
displayIcon: body.displayIcon,
modelMapping: body.modelMapping,
enabled: body.enabled ?? true,
}).run();
const routeId = Number(insertedRoute.lastInsertRowid || 0);
if (routeId <= 0) {
return { success: false, message: '创建路由失败' };
}
const route = await db.select().from(schema.tokenRoutes).where(eq(schema.tokenRoutes.id, routeId)).get();
if (!route) {
return { success: false, message: '创建路由失败' };
}
const insertedRoute = await db.insert(schema.tokenRoutes).values({
modelPattern: body.modelPattern,
displayName: body.displayName,
displayIcon: body.displayIcon,
modelMapping: body.modelMapping,
enabled: body.enabled ?? true,
}).run();
const routeId = Number(insertedRoute.lastInsertRowid || 0);
if (routeId <= 0) {
return { success: false, message: '创建路由失败' };
}
const route = await db.select().from(schema.tokenRoutes).where(eq(schema.tokenRoutes.id, routeId)).get();
if (!route) {
return { success: false, message: '创建路由失败' };
}
await populateRouteChannelsByModelPattern(route.id, body.modelPattern);
await populateRouteChannelsByModelPattern(route.id, body.modelPattern);
invalidateTokenRouterCache();
return route;
});
// Update a route
app.put<{ Params: { id: string }; Body: any }>('/api/routes/:id', async (request, reply) => {
const id = parseInt(request.params.id, 10);
const body = request.body as Record<string, unknown>;
const existingRoute = await db.select().from(schema.tokenRoutes).where(eq(schema.tokenRoutes.id, id)).get();
if (!existingRoute) {
return reply.code(404).send({ success: false, message: '路由不存在' });
}
const updates: Record<string, unknown> = {};
let nextModelPattern = existingRoute.modelPattern;
if (body.displayName !== undefined) updates.displayName = body.displayName;
if (body.displayIcon !== undefined) updates.displayIcon = body.displayIcon;
if (body.modelPattern !== undefined) {
nextModelPattern = String(body.modelPattern);
updates.modelPattern = nextModelPattern;
}
if (body.modelMapping !== undefined) updates.modelMapping = body.modelMapping;
if (body.enabled !== undefined) updates.enabled = body.enabled;
updates.updatedAt = new Date().toISOString();
await db.update(schema.tokenRoutes).set(updates).where(eq(schema.tokenRoutes.id, id)).run();
const modelPatternChanged = body.modelPattern !== undefined && nextModelPattern !== existingRoute.modelPattern;
const routeBehaviorChanged = modelPatternChanged || body.modelMapping !== undefined || body.enabled !== undefined;
if (modelPatternChanged) {
await rebuildAutomaticRouteChannelsByModelPattern(id, nextModelPattern);
}
if (routeBehaviorChanged) {
await clearRouteDecisionSnapshot(id);
}
invalidateTokenRouterCache();
return await db.select().from(schema.tokenRoutes).where(eq(schema.tokenRoutes.id, id)).get();
});
// Update a route
app.put<{ Params: { id: string }; Body: any }>('/api/routes/:id', async (request, reply) => {
const id = parseInt(request.params.id, 10);
const body = request.body as Record<string, unknown>;
const existingRoute = await db.select().from(schema.tokenRoutes).where(eq(schema.tokenRoutes.id, id)).get();
if (!existingRoute) {
return reply.code(404).send({ success: false, message: '路由不存在' });
}
const updates: Record<string, unknown> = {};
let nextModelPattern = existingRoute.modelPattern;
if (body.displayName !== undefined) updates.displayName = body.displayName;
if (body.displayIcon !== undefined) updates.displayIcon = body.displayIcon;
if (body.modelPattern !== undefined) {
nextModelPattern = String(body.modelPattern);
updates.modelPattern = nextModelPattern;
}
if (body.modelMapping !== undefined) updates.modelMapping = body.modelMapping;
if (body.enabled !== undefined) updates.enabled = body.enabled;
updates.updatedAt = new Date().toISOString();
await db.update(schema.tokenRoutes).set(updates).where(eq(schema.tokenRoutes.id, id)).run();
const modelPatternChanged = body.modelPattern !== undefined && nextModelPattern !== existingRoute.modelPattern;
const routeBehaviorChanged = modelPatternChanged || body.modelMapping !== undefined || body.enabled !== undefined;
if (modelPatternChanged) {
await rebuildAutomaticRouteChannelsByModelPattern(id, nextModelPattern);
}
if (routeBehaviorChanged) {
await clearRouteDecisionSnapshot(id);
}
invalidateTokenRouterCache();
return await db.select().from(schema.tokenRoutes).where(eq(schema.tokenRoutes.id, id)).get();
});
// Delete a route
app.delete<{ Params: { id: string } }>('/api/routes/:id', async (request) => {
@@ -600,13 +755,13 @@ export async function tokensRoutes(app: FastifyInstance) {
const sourceModel = typeof body.sourceModel === 'string'
? body.sourceModel.trim()
: (isExactModelPattern(route.modelPattern) ? route.modelPattern.trim() : '');
const effectiveTokenId = body.tokenId ?? await getDefaultTokenId(body.accountId);
const effectiveTokenId = body.tokenId ?? await getDefaultTokenId(body.accountId);
if (body.tokenId && !await checkTokenBelongsToAccount(body.tokenId, body.accountId)) {
if (body.tokenId && !await checkTokenBelongsToAccount(body.tokenId, body.accountId)) {
return reply.code(400).send({ success: false, message: '令牌不存在或不属于当前账号' });
}
if (isExactModelPattern(route.modelPattern) && effectiveTokenId && !await tokenSupportsModel(effectiveTokenId, route.modelPattern)) {
if (isExactModelPattern(route.modelPattern) && effectiveTokenId && !await tokenSupportsModel(effectiveTokenId, route.modelPattern)) {
return reply.code(400).send({ success: false, message: '该令牌不支持当前模型' });
}
@@ -622,26 +777,26 @@ export async function tokensRoutes(app: FastifyInstance) {
return reply.code(400).send({ success: false, message: '该来源模型的通道已存在' });
}
const insertedChannel = await db.insert(schema.routeChannels).values({
routeId,
accountId: body.accountId,
tokenId: body.tokenId,
sourceModel: sourceModel || null,
priority: body.priority ?? 0,
weight: body.weight ?? 10,
}).run();
const channelId = Number(insertedChannel.lastInsertRowid || 0);
if (channelId <= 0) {
return reply.code(500).send({ success: false, message: '创建通道失败' });
}
const created = await db.select().from(schema.routeChannels).where(eq(schema.routeChannels.id, channelId)).get();
if (!created) {
return reply.code(500).send({ success: false, message: '创建通道失败' });
}
await clearRouteDecisionSnapshot(routeId);
invalidateTokenRouterCache();
return created;
});
const insertedChannel = await db.insert(schema.routeChannels).values({
routeId,
accountId: body.accountId,
tokenId: body.tokenId,
sourceModel: sourceModel || null,
priority: body.priority ?? 0,
weight: body.weight ?? 10,
}).run();
const channelId = Number(insertedChannel.lastInsertRowid || 0);
if (channelId <= 0) {
return reply.code(500).send({ success: false, message: '创建通道失败' });
}
const created = await db.select().from(schema.routeChannels).where(eq(schema.routeChannels.id, channelId)).get();
if (!created) {
return reply.code(500).send({ success: false, message: '创建通道失败' });
}
await clearRouteDecisionSnapshot(routeId);
invalidateTokenRouterCache();
return created;
});
// Batch update channel priorities
app.put<{ Body: { updates: Array<{ id: number; priority: number }> } }>('/api/channels/batch', async (request, reply) => {
@@ -667,13 +822,13 @@ export async function tokensRoutes(app: FastifyInstance) {
}).where(eq(schema.routeChannels.id, update.id)).run();
}
const updatedChannels = await db.select().from(schema.routeChannels)
.where(inArray(schema.routeChannels.id, channelIds))
.all();
await clearRouteDecisionSnapshots(existingChannels.map((channel) => channel.routeId));
invalidateTokenRouterCache();
return { success: true, channels: updatedChannels };
});
const updatedChannels = await db.select().from(schema.routeChannels)
.where(inArray(schema.routeChannels.id, channelIds))
.all();
await clearRouteDecisionSnapshots(existingChannels.map((channel) => channel.routeId));
invalidateTokenRouterCache();
return { success: true, channels: updatedChannels };
});
// Update a channel
app.put<{ Params: { channelId: string }; Body: any }>('/api/channels/:channelId', async (request, reply) => {
@@ -692,16 +847,16 @@ export async function tokensRoutes(app: FastifyInstance) {
if (body.tokenId !== undefined && body.tokenId !== null) {
const tokenId = Number(body.tokenId);
if (!Number.isFinite(tokenId) || !await checkTokenBelongsToAccount(tokenId, channel.accountId)) {
if (!Number.isFinite(tokenId) || !await checkTokenBelongsToAccount(tokenId, channel.accountId)) {
return reply.code(400).send({ success: false, message: '令牌不存在或不属于通道账号' });
}
}
const nextTokenId = body.tokenId === undefined
? (channel.tokenId ?? await getDefaultTokenId(channel.accountId))
: (body.tokenId === null ? await getDefaultTokenId(channel.accountId) : Number(body.tokenId));
const nextTokenId = body.tokenId === undefined
? (channel.tokenId ?? await getDefaultTokenId(channel.accountId))
: (body.tokenId === null ? await getDefaultTokenId(channel.accountId) : Number(body.tokenId));
if (isExactModelPattern(route.modelPattern) && nextTokenId && !await tokenSupportsModel(nextTokenId, route.modelPattern)) {
if (isExactModelPattern(route.modelPattern) && nextTokenId && !await tokenSupportsModel(nextTokenId, route.modelPattern)) {
return reply.code(400).send({ success: false, message: '该令牌不支持当前模型' });
}
@@ -714,24 +869,24 @@ export async function tokensRoutes(app: FastifyInstance) {
for (const key of ['priority', 'weight', 'enabled', 'tokenId']) {
if (body[key] !== undefined) updates[key] = body[key];
}
await db.update(schema.routeChannels).set(updates).where(eq(schema.routeChannels.id, channelId)).run();
await clearRouteDecisionSnapshot(channel.routeId);
invalidateTokenRouterCache();
return await db.select().from(schema.routeChannels).where(eq(schema.routeChannels.id, channelId)).get();
});
// Delete a channel
app.delete<{ Params: { channelId: string } }>('/api/channels/:channelId', async (request) => {
const channelId = parseInt(request.params.channelId, 10);
const channel = await db.select().from(schema.routeChannels).where(eq(schema.routeChannels.id, channelId)).get();
await db.delete(schema.routeChannels).where(eq(schema.routeChannels.id, channelId)).run();
if (channel) {
await clearRouteDecisionSnapshot(channel.routeId);
}
invalidateTokenRouterCache();
return { success: true };
});
await db.update(schema.routeChannels).set(updates).where(eq(schema.routeChannels.id, channelId)).run();
await clearRouteDecisionSnapshot(channel.routeId);
invalidateTokenRouterCache();
return await db.select().from(schema.routeChannels).where(eq(schema.routeChannels.id, channelId)).get();
});
// Delete a channel
app.delete<{ Params: { channelId: string } }>('/api/channels/:channelId', async (request) => {
const channelId = parseInt(request.params.channelId, 10);
const channel = await db.select().from(schema.routeChannels).where(eq(schema.routeChannels.id, channelId)).get();
await db.delete(schema.routeChannels).where(eq(schema.routeChannels.id, channelId)).run();
if (channel) {
await clearRouteDecisionSnapshot(channel.routeId);
}
invalidateTokenRouterCache();
return { success: true };
});
// Rebuild routes/channels from model availability.
app.post<{ Body?: { refreshModels?: boolean; wait?: boolean } }>('/api/routes/rebuild', async (request, reply) => {