| import { Router } from 'express'; |
| import type { Request, Response } from 'express'; |
| import { z } from 'zod'; |
| import { getDb } from '../db/index.js'; |
| import { getAllPenalties, getCustomWeights, getRoutingScores, getRoutingStrategy, setCustomWeights, setRoutingStrategy } from '../services/router.js'; |
| import { BANDIT_PRESETS, type RoutingStrategy } from '../services/scoring.js'; |
| import { parseBudget } from '../lib/budget.js'; |
|
|
| export const fallbackRouter = Router(); |
|
|
| |
| |
| |
| |
| fallbackRouter.get('/routing', async (_req: Request, res: Response) => { |
| res.json({ ...(await getRoutingScores()), customWeights: await getCustomWeights() }); |
| }); |
|
|
| const routingSchema = z.object({ |
| strategy: z.enum(['priority', 'balanced', 'smartest', 'fastest', 'reliable', 'custom']), |
| |
| |
| weights: z.object({ |
| reliability: z.number().min(0).max(1), |
| speed: z.number().min(0).max(1), |
| intelligence: z.number().min(0).max(1), |
| }).refine(w => w.reliability + w.speed + w.intelligence > 0, { |
| message: 'weights must not all be zero', |
| }).optional(), |
| }); |
|
|
| |
| |
| |
| fallbackRouter.put('/routing', async (req: Request, res: Response) => { |
| const parsed = routingSchema.safeParse(req.body); |
| if (!parsed.success) { |
| res.status(400).json({ error: { message: parsed.error.errors.map(e => e.message).join(', ') } }); |
| return; |
| } |
| if (parsed.data.strategy === 'custom' && parsed.data.weights) { |
| await setCustomWeights(parsed.data.weights); |
| } |
| await setRoutingStrategy(parsed.data.strategy as RoutingStrategy); |
| res.json({ strategy: await getRoutingStrategy(), presets: BANDIT_PRESETS, customWeights: await getCustomWeights() }); |
| }); |
|
|
| |
| fallbackRouter.get('/', async (_req: Request, res: Response) => { |
| const db = getDb(); |
| const rows = await db.all(` |
| SELECT fc.model_db_id, fc.priority, fc.enabled, |
| m.platform, m.model_id, m.display_name, m.intelligence_rank, |
| m.speed_rank, m.size_label, m.rpm_limit, m.rpd_limit, |
| m.monthly_token_budget, m.supports_vision, m.supports_tools |
| FROM fallback_config fc |
| JOIN models m ON m.id = fc.model_db_id |
| ORDER BY fc.priority ASC |
| `); |
|
|
| |
| const keyCounts = await db.all<{ platform: string; count: number }>(` |
| SELECT platform, COUNT(*) as count |
| FROM api_keys WHERE enabled = 1 |
| GROUP BY platform |
| `); |
| const keyCountMap = new Map(keyCounts.map(k => [k.platform, k.count])); |
|
|
| |
| const penalties = getAllPenalties(); |
| const penaltyMap = new Map(penalties.map(p => [p.modelDbId, p])); |
|
|
| res.json(rows.map(r => { |
| const penalty = penaltyMap.get(r.model_db_id); |
| return { |
| modelDbId: r.model_db_id, |
| priority: r.priority, |
| effectivePriority: r.priority + (penalty?.penalty ?? 0), |
| penalty: penalty?.penalty ?? 0, |
| rateLimitHits: penalty?.count ?? 0, |
| enabled: r.enabled === 1, |
| platform: r.platform, |
| modelId: r.model_id, |
| displayName: r.display_name, |
| intelligenceRank: r.intelligence_rank, |
| speedRank: r.speed_rank, |
| sizeLabel: r.size_label, |
| rpmLimit: r.rpm_limit, |
| rpdLimit: r.rpd_limit, |
| monthlyTokenBudget: r.monthly_token_budget, |
| supportsVision: r.supports_vision === 1, |
| supportsTools: r.supports_tools === 1, |
| keyCount: keyCountMap.get(r.platform) ?? 0, |
| }; |
| })); |
| }); |
|
|
| const updateSchema = z.array(z.object({ |
| modelDbId: z.number(), |
| priority: z.number(), |
| enabled: z.boolean(), |
| })); |
|
|
| |
| fallbackRouter.put('/', async (req: Request, res: Response) => { |
| const parsed = updateSchema.safeParse(req.body); |
| if (!parsed.success) { |
| res.status(400).json({ error: { message: parsed.error.errors.map(e => e.message).join(', ') } }); |
| return; |
| } |
|
|
| const db = getDb(); |
| const statements = parsed.data.map(entry => ({ |
| sql: 'UPDATE fallback_config SET priority = ?, enabled = ? WHERE model_db_id = ?', |
| args: [entry.priority, entry.enabled ? 1 : 0, entry.modelDbId], |
| })); |
| await db.batch(statements); |
|
|
| res.json({ success: true }); |
| }); |
|
|
| |
| |
| |
| |
| |
| const INTELLIGENCE_TIER = |
| "CASE m.size_label WHEN 'Frontier' THEN 1 WHEN 'Large' THEN 2 WHEN 'Medium' THEN 3 WHEN 'Small' THEN 4 ELSE 5 END"; |
|
|
| |
| |
| const SORT_PRESETS: Record<string, string> = { |
| intelligence: `${INTELLIGENCE_TIER} ASC, m.intelligence_rank ASC`, |
| speed: 'm.speed_rank ASC', |
| budget: "CASE m.monthly_token_budget WHEN '~120M' THEN 1 WHEN '~50-100M' THEN 2 WHEN '~30M' THEN 3 WHEN '~18-45M' THEN 4 WHEN '~18M' THEN 5 WHEN '~15M' THEN 6 WHEN '~12M' THEN 7 WHEN '~6M' THEN 8 WHEN '~5-10M' THEN 9 WHEN '~4M' THEN 10 ELSE 11 END ASC", |
| }; |
|
|
| fallbackRouter.post('/sort/:preset', async (req: Request, res: Response) => { |
| const preset = String(req.params.preset); |
| const orderBy = SORT_PRESETS[preset]; |
| if (!orderBy) { |
| res.status(400).json({ error: { message: `Unknown preset: ${preset}. Use: intelligence, speed, budget` } }); |
| return; |
| } |
|
|
| const db = getDb(); |
| const models = await db.all<{ id: number }>(`SELECT m.id FROM models m ORDER BY ${orderBy}`); |
|
|
| const statements = models.map((m, i) => ({ |
| sql: 'UPDATE fallback_config SET priority = ? WHERE model_db_id = ?', |
| args: [i + 1, m.id], |
| })); |
| await db.batch(statements); |
|
|
| res.json({ success: true, preset }); |
| }); |
|
|
| |
| fallbackRouter.get('/token-usage', async (_req: Request, res: Response) => { |
| const db = getDb(); |
|
|
| |
| const platforms = await db.all<{ platform: string }>(` |
| SELECT DISTINCT ak.platform |
| FROM api_keys ak |
| WHERE ak.enabled = 1 |
| `); |
| const platformSet = new Set(platforms.map(p => p.platform)); |
|
|
| |
| const models = await db.all<{ platform: string; model_id: string; display_name: string; monthly_token_budget: string; priority: number }>(` |
| SELECT m.platform, m.model_id, m.display_name, m.monthly_token_budget, |
| fc.priority |
| FROM models m |
| JOIN fallback_config fc ON fc.model_db_id = m.id |
| WHERE m.enabled = 1 |
| ORDER BY fc.priority ASC |
| `); |
|
|
| |
| const modelBudgets = models |
| .filter(m => platformSet.has(m.platform)) |
| .map(m => ({ |
| displayName: m.display_name, |
| platform: m.platform, |
| budget: parseBudget(m.monthly_token_budget), |
| })); |
|
|
| const totalBudget = modelBudgets.reduce((s, m) => s + m.budget, 0); |
|
|
| |
| const usage = await db.get<{ total_used: number }>(` |
| SELECT |
| COALESCE(SUM(input_tokens + output_tokens), 0) as total_used |
| FROM requests |
| WHERE created_at >= datetime('now', 'start of month') |
| AND request_type = 'chat' |
| `); |
|
|
| res.json({ |
| totalBudget, |
| totalUsed: usage?.total_used ?? 0, |
| models: modelBudgets, |
| }); |
| }); |
|
|