llm-simple-router 1.0.11 → 1.0.13

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (84) hide show
  1. package/dist/admin/dashboard.d.ts +2 -0
  2. package/dist/admin/dashboard.js +112 -70
  3. package/dist/admin/groups.js +10 -1
  4. package/dist/admin/logs.js +30 -1
  5. package/dist/admin/monitor.js +16 -0
  6. package/dist/admin/providers.d.ts +35 -0
  7. package/dist/admin/providers.js +53 -32
  8. package/dist/admin/proxy-enhancement.js +37 -0
  9. package/dist/admin/quick-setup.js +33 -0
  10. package/dist/admin/retry-rules.js +13 -1
  11. package/dist/admin/router-keys.js +6 -0
  12. package/dist/admin/routes.js +1 -1
  13. package/dist/admin/schedules.js +11 -1
  14. package/dist/admin/settings.js +33 -0
  15. package/dist/db/index.d.ts +1 -1
  16. package/dist/db/index.js +1 -1
  17. package/dist/db/stats.d.ts +6 -0
  18. package/dist/db/stats.js +1 -1
  19. package/frontend-dist/assets/{AuthLayout-BNvGjJv3.js → AuthLayout-C7E43a6p.js} +1 -1
  20. package/frontend-dist/assets/{Card-BNz2IkLE.js → Card-L1MI4nan.js} +1 -1
  21. package/frontend-dist/assets/{CardContent-DszVPJgx.js → CardContent-DUzyQmbU.js} +1 -1
  22. package/frontend-dist/assets/{CardTitle-BeBkx0ns.js → CardTitle-oL3GJ7xA.js} +1 -1
  23. package/frontend-dist/assets/{CascadingModelSelect-Z7-ur9_p.js → CascadingModelSelect-C_Ydm0fg.js} +1 -1
  24. package/frontend-dist/assets/{Checkbox-CAQnc0qV.js → Checkbox-OkYUPlFe.js} +1 -1
  25. package/frontend-dist/assets/{CollapsibleContent-CHlSINCS.js → CollapsibleContent-4ZwwyZPz.js} +1 -1
  26. package/frontend-dist/assets/{CollapsibleTrigger-CJMUoPRa.js → CollapsibleTrigger-Cg651F6-.js} +1 -1
  27. package/frontend-dist/assets/{ConcurrencyControl-BcUA816m.js → ConcurrencyControl-LFw4H_oP.js} +1 -1
  28. package/frontend-dist/assets/{Dashboard-DXvcAfAx.js → Dashboard-Bqups5aa.js} +2 -2
  29. package/frontend-dist/assets/{Input-Dr2XqfSh.js → Input-CLu4bS6g.js} +1 -1
  30. package/frontend-dist/assets/{Label-Ct_DR7Ui.js → Label-DVu1uVI7.js} +1 -1
  31. package/frontend-dist/assets/{Login-C8_p8684.js → Login--CFSqs7j.js} +1 -1
  32. package/frontend-dist/assets/Logs-CJPid8DO.js +1 -0
  33. package/frontend-dist/assets/ModelMappings-BTM9yiL6.js +1 -0
  34. package/frontend-dist/assets/Monitor-CBLKclHj.js +1 -0
  35. package/frontend-dist/assets/Providers-5RjiOHfn.js +1 -0
  36. package/frontend-dist/assets/ProxyEnhancement-XKgw6IoB.js +1 -0
  37. package/frontend-dist/assets/QuickSetup-CISVMd51.js +1 -0
  38. package/frontend-dist/assets/RetryRules-DVSEfmo2.js +1 -0
  39. package/frontend-dist/assets/RouterKeys-Cx2n1D0b.js +1 -0
  40. package/frontend-dist/assets/{RouterKeys-B0enpkQK.css → RouterKeys-D8tI3eHr.css} +1 -1
  41. package/frontend-dist/assets/{RovingFocusItem-DlOsi-lb.js → RovingFocusItem-CB_XB24W.js} +1 -1
  42. package/frontend-dist/assets/Schedules-BMD8vrVd.js +1 -0
  43. package/frontend-dist/assets/{Separator-BO94Jk9p.js → Separator-DHTeoMaB.js} +1 -1
  44. package/frontend-dist/assets/Settings-C3yzrB1_.js +6 -0
  45. package/frontend-dist/assets/{Setup-mAyd7FhD.js → Setup-Cgf37mQl.js} +1 -1
  46. package/frontend-dist/assets/{Skeleton-Di0WMQf-.js → Skeleton-DcA4Afm_.js} +1 -1
  47. package/frontend-dist/assets/{Switch-BS1G55M2.js → Switch-BDu-VJJd.js} +1 -1
  48. package/frontend-dist/assets/{TableHeader-jCTjghgy.js → TableHeader-ncK52pod.js} +1 -1
  49. package/frontend-dist/assets/{TabsTrigger-B5HyH2xO.js → TabsTrigger-DeTfRTma.js} +1 -1
  50. package/frontend-dist/assets/{UnifiedRequestDialog-CQ5Rzs11.js → UnifiedRequestDialog-CLHb7t0f.js} +3 -3
  51. package/frontend-dist/assets/{VisuallyHiddenInput-DiW4Si0N.js → VisuallyHiddenInput-DcH9Mxb9.js} +1 -1
  52. package/frontend-dist/assets/arrow-down-CuC5m7VK.js +1 -0
  53. package/frontend-dist/assets/{badge-FksLt30f.js → badge-DhXKxnV5.js} +1 -1
  54. package/frontend-dist/assets/{button-BCxYWEaM.js → button-BuzJlj9X.js} +2 -2
  55. package/frontend-dist/assets/chevron-right-LdKvgE7W.js +1 -0
  56. package/frontend-dist/assets/{dialog-B0wgNzud.js → dialog-CtPySx34.js} +1 -1
  57. package/frontend-dist/assets/{image-DlPnda0z.js → image-BkW18kNK.js} +1 -1
  58. package/frontend-dist/assets/{index-CwjudU09.js → index-BjnFvPid.js} +2 -2
  59. package/frontend-dist/assets/{model-patches-Dv2jfKSp.js → model-patches-DQFF0Cuh.js} +1 -1
  60. package/frontend-dist/assets/{pencil-Y6MJERvc.js → pencil-i0P0IRz5.js} +1 -1
  61. package/frontend-dist/assets/plus-fHe1ysx9.js +1 -0
  62. package/frontend-dist/assets/search-Dast7lfE.js +1 -0
  63. package/frontend-dist/assets/{sparkles-C44_iMrx.js → sparkles-BGb3coPL.js} +1 -1
  64. package/frontend-dist/assets/{transform-domain-FnCfUKrh.js → transform-domain-pYyIPtkg.js} +1 -1
  65. package/frontend-dist/assets/{trash-2-BEOdZk69.js → trash-2-DhD7GboJ.js} +1 -1
  66. package/frontend-dist/assets/{useClipboard-Bw6GLR6I.js → useClipboard-tJg52v7m.js} +1 -1
  67. package/frontend-dist/assets/{useLogRetention-CQHD99Fy.js → useLogRetention-D7ZaYfCW.js} +1 -1
  68. package/frontend-dist/assets/{useProviderGroups-Bz7Yz1b7.js → useProviderGroups-DMuF_-28.js} +1 -1
  69. package/frontend-dist/index.html +2 -2
  70. package/package.json +1 -1
  71. package/frontend-dist/assets/Logs-BDrXnYb1.js +0 -1
  72. package/frontend-dist/assets/ModelMappings-CeLps4LM.js +0 -1
  73. package/frontend-dist/assets/Monitor-By1ywNWD.js +0 -1
  74. package/frontend-dist/assets/Providers-Cp-gomQY.js +0 -1
  75. package/frontend-dist/assets/ProxyEnhancement-D5jsMnL3.js +0 -1
  76. package/frontend-dist/assets/QuickSetup-1ADLBexY.js +0 -1
  77. package/frontend-dist/assets/RetryRules-Dk0OxVLs.js +0 -1
  78. package/frontend-dist/assets/RouterKeys-BvD4POUA.js +0 -1
  79. package/frontend-dist/assets/Schedules-CVJcpHlE.js +0 -1
  80. package/frontend-dist/assets/Settings-MYcQoEZw.js +0 -6
  81. package/frontend-dist/assets/arrow-down-BsHwq97B.js +0 -1
  82. package/frontend-dist/assets/chevron-right-C3U2yjrf.js +0 -1
  83. package/frontend-dist/assets/plus-FNO9X5JG.js +0 -1
  84. package/frontend-dist/assets/search-DpWQ2wvO.js +0 -1
@@ -1,7 +1,9 @@
1
1
  import { FastifyPluginCallback } from "fastify";
2
2
  import Database from "better-sqlite3";
3
+ import type { StateRegistry } from "../core/registry.js";
3
4
  interface DashboardRoutesOptions {
4
5
  db: Database.Database;
6
+ stateRegistry?: StateRegistry;
5
7
  }
6
8
  export declare const adminDashboardRoutes: FastifyPluginCallback<DashboardRoutesOptions>;
7
9
  export {};
@@ -1,5 +1,7 @@
1
1
  import { Type } from "@sinclair/typebox";
2
- import { getStats, getMetricsSummary, getMetricsTimeseries, getClientTypeBreakdown } from "../db/index.js";
2
+ import { getStats, getMetricsSummary, getMetricsTimeseries, getClientTypeBreakdown, getAllProviders, getAllRouterKeys, computeBucketBoundary } from "../db/index.js";
3
+ import { getSetting } from "../db/settings.js";
4
+ import { serializeProviders } from "./providers.js";
3
5
  const OverviewQuerySchema = Type.Object({
4
6
  provider_id: Type.Optional(Type.String()),
5
7
  backend_model: Type.Optional(Type.String()),
@@ -10,84 +12,124 @@ const OverviewQuerySchema = Type.Object({
10
12
  });
11
13
  const PERCENT_MULTIPLIER = 100;
12
14
  const PCT_ROUND_DIGITS = 10;
13
- const PROVIDER_TOKEN_LOOKBACK_DAYS = 30;
15
+ /** Compute overview stats (reused by both /overview and /init) */
16
+ async function computeOverview(db, query) {
17
+ const startTime = query.start_time;
18
+ const endTime = query.end_time;
19
+ const providerId = query.provider_id || undefined;
20
+ const backendModel = query.backend_model || undefined;
21
+ const routerKeyId = query.router_key_id || undefined;
22
+ const clientType = query.client_type || undefined;
23
+ // 1. Current period stats
24
+ const stats = getStats(db, startTime, endTime, routerKeyId, providerId, backendModel, clientType);
25
+ // 2. Previous period stats (same duration, immediately before current window)
26
+ const durationMs = new Date(endTime).getTime() - new Date(startTime).getTime();
27
+ const prevEnd = startTime;
28
+ const prevStart = new Date(new Date(startTime).getTime() - durationMs).toISOString();
29
+ const prevStats = durationMs > 0
30
+ ? getStats(db, prevStart, prevEnd, routerKeyId, providerId, backendModel, clientType)
31
+ : null;
32
+ // 3. Timeseries (tps, input_tokens, output_tokens)
33
+ const legacyPeriod = "30d";
34
+ const [tpsRes, inputRes, outputRes] = await Promise.allSettled([
35
+ getMetricsTimeseries(db, legacyPeriod, "total_tps", providerId, backendModel, routerKeyId, startTime, endTime, clientType),
36
+ getMetricsTimeseries(db, legacyPeriod, "input_tokens", providerId, backendModel, routerKeyId, startTime, endTime, clientType),
37
+ getMetricsTimeseries(db, legacyPeriod, "output_tokens", providerId, backendModel, routerKeyId, startTime, endTime, clientType),
38
+ ]);
39
+ const tpsRes_ = tpsRes.status === "fulfilled" ? tpsRes.value : [];
40
+ const inputRes_ = inputRes.status === "fulfilled" ? inputRes.value : [];
41
+ const outputRes_ = outputRes.status === "fulfilled" ? outputRes.value : [];
42
+ // 4. Cache hit rate + client type breakdown
43
+ const summary = getMetricsSummary(db, legacyPeriod, providerId, backendModel, routerKeyId, startTime, endTime, clientType);
44
+ const totalInputTokens = summary.reduce((sum, r) => sum + r.total_input_tokens, 0);
45
+ const totalCacheHitTokens = summary.reduce((sum, r) => sum + r.total_cache_hit_tokens, 0);
46
+ const cacheHitRate = totalInputTokens > 0
47
+ ? Math.round(totalCacheHitTokens * PERCENT_MULTIPLIER / totalInputTokens * PCT_ROUND_DIGITS) / PCT_ROUND_DIGITS
48
+ : 0;
49
+ const breakdown = getClientTypeBreakdown(db, legacyPeriod, providerId, backendModel, routerKeyId, startTime, endTime);
50
+ // 5. Provider token summary (same time range as overview)
51
+ const providerTokenSummary = getProviderTokenSummary(db, startTime, endTime);
52
+ return {
53
+ stats: {
54
+ totalRequests: stats.totalRequests,
55
+ successRate: stats.successRate,
56
+ avgTps: stats.avgTps,
57
+ totalInputTokens: stats.totalInputTokens,
58
+ totalOutputTokens: stats.totalOutputTokens,
59
+ startTime,
60
+ endTime,
61
+ },
62
+ prev_stats: prevStats ? {
63
+ totalRequests: prevStats.totalRequests,
64
+ successRate: prevStats.successRate,
65
+ avgTps: prevStats.avgTps,
66
+ totalInputTokens: prevStats.totalInputTokens,
67
+ totalOutputTokens: prevStats.totalOutputTokens,
68
+ } : null,
69
+ cache_hit_rate: cacheHitRate,
70
+ client_type_breakdown: breakdown,
71
+ timeseries: {
72
+ tps: tpsRes_,
73
+ input_tokens: inputRes_,
74
+ output_tokens: outputRes_,
75
+ },
76
+ provider_token_summary: providerTokenSummary,
77
+ };
78
+ }
14
79
  export const adminDashboardRoutes = (app, options, done) => {
15
- const { db } = options;
16
- app.get("/admin/api/dashboard/overview", { schema: { querystring: OverviewQuerySchema } }, async (request, reply) => {
80
+ const { db, stateRegistry } = options;
81
+ app.get("/admin/api/dashboard/init", { schema: { querystring: OverviewQuerySchema } }, async (request, reply) => {
17
82
  const query = request.query;
18
- const startTime = query.start_time;
19
- const endTime = query.end_time;
20
- const providerId = query.provider_id || undefined;
21
- const backendModel = query.backend_model || undefined;
22
- const routerKeyId = query.router_key_id || undefined;
23
- const clientType = query.client_type || undefined;
24
- // 1. Current period stats
25
- const stats = getStats(db, startTime, endTime, routerKeyId, providerId, backendModel, clientType);
26
- // 2. Previous period stats (same duration, immediately before current window)
27
- const durationMs = new Date(endTime).getTime() - new Date(startTime).getTime();
28
- const prevEnd = startTime;
29
- const prevStart = new Date(new Date(startTime).getTime() - durationMs).toISOString();
30
- const prevStats = durationMs > 0
31
- ? getStats(db, prevStart, prevEnd, routerKeyId, providerId, backendModel, clientType)
32
- : null;
33
- // 3. Timeseries (tps, input_tokens, output_tokens)
34
- const legacyPeriod = "30d";
35
- const [tpsRes, inputRes, outputRes] = await Promise.allSettled([
36
- getMetricsTimeseries(db, legacyPeriod, "total_tps", providerId, backendModel, routerKeyId, startTime, endTime, clientType),
37
- getMetricsTimeseries(db, legacyPeriod, "input_tokens", providerId, backendModel, routerKeyId, startTime, endTime, clientType),
38
- getMetricsTimeseries(db, legacyPeriod, "output_tokens", providerId, backendModel, routerKeyId, startTime, endTime, clientType),
83
+ const [providersResult, routerKeysResult, overviewResult] = await Promise.allSettled([
84
+ (async () => {
85
+ const encryptionKey = getSetting(db, "encryption_key");
86
+ const providers = getAllProviders(db);
87
+ return serializeProviders(db, providers, encryptionKey, (id) => stateRegistry?.getProviderStatus(id) ?? { active: 0, queued: 0 });
88
+ })(),
89
+ Promise.resolve(getAllRouterKeys(db).map(rk => ({ id: rk.id, name: rk.name }))),
90
+ (async () => computeOverview(db, query))(),
39
91
  ]);
40
- const tpsRes_ = tpsRes.status === "fulfilled" ? tpsRes.value : [];
41
- const inputRes_ = inputRes.status === "fulfilled" ? inputRes.value : [];
42
- const outputRes_ = outputRes.status === "fulfilled" ? outputRes.value : [];
43
- // 4. Cache hit rate + client type breakdown
44
- const summary = getMetricsSummary(db, legacyPeriod, providerId, backendModel, routerKeyId, startTime, endTime, clientType);
45
- const totalInputTokens = summary.reduce((sum, r) => sum + r.total_input_tokens, 0);
46
- const totalCacheHitTokens = summary.reduce((sum, r) => sum + r.total_cache_hit_tokens, 0);
47
- const cacheHitRate = totalInputTokens > 0
48
- ? Math.round(totalCacheHitTokens * PERCENT_MULTIPLIER / totalInputTokens * PCT_ROUND_DIGITS) / PCT_ROUND_DIGITS
49
- : 0;
50
- const breakdown = getClientTypeBreakdown(db, legacyPeriod, providerId, backendModel, routerKeyId, startTime, endTime);
51
- // 5. Provider token summary (for provider sorting/labels in frontend)
52
- const providerTokenSummary = getProviderTokenSummary(db);
53
92
  return reply.send({
54
- stats: {
55
- totalRequests: stats.totalRequests,
56
- successRate: stats.successRate,
57
- avgTps: stats.avgTps,
58
- totalInputTokens: stats.totalInputTokens,
59
- totalOutputTokens: stats.totalOutputTokens,
60
- startTime,
61
- endTime,
62
- },
63
- prev_stats: prevStats ? {
64
- totalRequests: prevStats.totalRequests,
65
- successRate: prevStats.successRate,
66
- avgTps: prevStats.avgTps,
67
- totalInputTokens: prevStats.totalInputTokens,
68
- totalOutputTokens: prevStats.totalOutputTokens,
69
- } : null,
70
- cache_hit_rate: cacheHitRate,
71
- client_type_breakdown: breakdown,
72
- timeseries: {
73
- tps: tpsRes_,
74
- input_tokens: inputRes_,
75
- output_tokens: outputRes_,
76
- },
77
- provider_token_summary: providerTokenSummary,
93
+ providers: providersResult.status === "fulfilled" ? providersResult.value : null,
94
+ router_keys: routerKeysResult.status === "fulfilled" ? routerKeysResult.value : null,
95
+ ...(overviewResult.status === "fulfilled" ? overviewResult.value : {
96
+ stats: null, prev_stats: null, cache_hit_rate: null,
97
+ client_type_breakdown: null, timeseries: null, provider_token_summary: null,
98
+ }),
78
99
  });
79
100
  });
101
+ app.get("/admin/api/dashboard/overview", { schema: { querystring: OverviewQuerySchema } }, async (request, reply) => {
102
+ const query = request.query;
103
+ return reply.send(await computeOverview(db, query));
104
+ });
80
105
  done();
81
106
  };
82
- /** Get per-provider total input tokens for last 30 days (for sorting/labels) */
83
- function getProviderTokenSummary(db) {
84
- const rows = db.prepare(`SELECT provider_id, COALESCE(SUM(sum_input_tokens), 0) AS total_input_tokens
85
- FROM metrics_10min
86
- WHERE bucket_time >= datetime('now', '-' || ? || ' days')
87
- GROUP BY provider_id`).all(PROVIDER_TOKEN_LOOKBACK_DAYS);
107
+ /** Get per-provider total input tokens for the given time range (for sorting/labels).
108
+ * Uses the same cross-boundary merge as getStats(): metrics_10min (settled) + request_metrics (current bucket).
109
+ */
110
+ function getProviderTokenSummary(db, startTime, endTime) {
88
111
  const result = {};
89
- for (const r of rows) {
90
- result[r.provider_id] = r.total_input_tokens;
112
+ const boundary = computeBucketBoundary();
113
+ // 1. Aggregated data from metrics_10min (everything before the current bucket)
114
+ const aggEnd = endTime <= boundary ? endTime : boundary;
115
+ if (startTime < aggEnd) {
116
+ const rows = db.prepare(`SELECT provider_id, COALESCE(SUM(sum_input_tokens), 0) AS total_input_tokens
117
+ FROM metrics_10min
118
+ WHERE bucket_time >= datetime(?) AND bucket_time < datetime(?)
119
+ GROUP BY provider_id`).all(startTime, aggEnd);
120
+ for (const r of rows) {
121
+ result[r.provider_id] = (result[r.provider_id] ?? 0) + r.total_input_tokens;
122
+ }
123
+ }
124
+ // 2. Real-time data from request_metrics (current bucket to now)
125
+ if (endTime > boundary && boundary > startTime) {
126
+ const detailRows = db.prepare(`SELECT provider_id, COALESCE(SUM(input_tokens), 0) AS total_input_tokens
127
+ FROM request_metrics
128
+ WHERE created_at >= datetime(?) AND created_at < datetime(?)
129
+ GROUP BY provider_id`).all(boundary, endTime);
130
+ for (const r of detailRows) {
131
+ result[r.provider_id] = (result[r.provider_id] ?? 0) + r.total_input_tokens;
132
+ }
91
133
  }
92
134
  return result;
93
135
  }
@@ -1,5 +1,7 @@
1
1
  import { Type } from "@sinclair/typebox";
2
- import { getAllMappingGroups, createMappingGroup, updateMappingGroup, deleteMappingGroup, getProviderById, getMappingGroupById, } from "../db/index.js";
2
+ import { getAllMappingGroups, createMappingGroup, updateMappingGroup, deleteMappingGroup, getProviderById, getMappingGroupById, getAllProviders, } from "../db/index.js";
3
+ import { getSetting } from "../db/settings.js";
4
+ import { serializeProviders } from "./providers.js";
3
5
  import { HTTP_BAD_REQUEST, HTTP_CREATED, HTTP_CONFLICT, HTTP_NOT_FOUND } from "./constants.js";
4
6
  import { parseModels } from "../config/model-context.js";
5
7
  import { API_CODE, apiError } from "./api-response.js";
@@ -87,6 +89,13 @@ export const adminGroupRoutes = (app, options, done) => {
87
89
  const groups = getAllMappingGroups(db);
88
90
  return reply.send(groups);
89
91
  });
92
+ app.get("/admin/api/mapping-groups/init", async (_request, reply) => {
93
+ const groups = getAllMappingGroups(db);
94
+ const encryptionKey = getSetting(db, "encryption_key");
95
+ const providers = getAllProviders(db);
96
+ const serialized = serializeProviders(db, providers, encryptionKey);
97
+ return reply.send({ groups, providers: serialized });
98
+ });
90
99
  app.post("/admin/api/mapping-groups", { schema: { body: CreateGroupSchema } }, async (request, reply) => {
91
100
  const body = request.body;
92
101
  const validationError = validateRule(db, body.rule);
@@ -1,5 +1,6 @@
1
1
  import { Type } from "@sinclair/typebox";
2
- import { getRequestLogs, getRequestLogsGrouped, getRequestLogById, getRequestLogChildren, deleteLogsBefore, extractThinkingLevel } from "../db/index.js";
2
+ import { getRequestLogs, getRequestLogsGrouped, getRequestLogById, getRequestLogChildren, deleteLogsBefore, extractThinkingLevel, getAllProviders, getAllRouterKeys, getAllMappingGroups } from "../db/index.js";
3
+ import { getLogRetentionDays } from "../db/settings.js";
3
4
  import { HTTP_NOT_FOUND } from "./constants.js";
4
5
  import { API_CODE, apiError } from "./api-response.js";
5
6
  const LogQuerySchema = Type.Object({
@@ -22,6 +23,34 @@ const DeleteLogsBeforeSchema = Type.Object({
22
23
  const DEFAULT_LOG_VIEW = "flat";
23
24
  export const adminLogRoutes = (app, options, done) => {
24
25
  const { db, logFileWriter } = options;
26
+ app.get("/admin/api/logs/init", async () => {
27
+ const [providersResult, routerKeysResult, groupsResult, retentionResult] = await Promise.allSettled([
28
+ Promise.resolve(getAllProviders(db).map(p => ({ id: p.id, name: p.name }))),
29
+ Promise.resolve(getAllRouterKeys(db).map(rk => ({ id: rk.id, name: rk.name }))),
30
+ (async () => {
31
+ const groups = getAllMappingGroups(db);
32
+ const clientModels = [...new Set(groups.filter(g => g.is_active).map(g => g.client_model))].sort();
33
+ const backendModels = [...new Set(groups.flatMap(g => {
34
+ try {
35
+ const rule = JSON.parse(g.rule);
36
+ return (Array.isArray(rule.targets) ? rule.targets : []).map((t) => t.backend_model);
37
+ }
38
+ catch {
39
+ return [];
40
+ }
41
+ }).filter(Boolean))].sort();
42
+ return { client_models: clientModels, backend_models: backendModels };
43
+ })(),
44
+ Promise.resolve(getLogRetentionDays(db)),
45
+ ]);
46
+ return {
47
+ providers: providersResult.status === "fulfilled" ? providersResult.value : null,
48
+ router_keys: routerKeysResult.status === "fulfilled" ? routerKeysResult.value : null,
49
+ client_models: groupsResult.status === "fulfilled" ? groupsResult.value.client_models : null,
50
+ backend_models: groupsResult.status === "fulfilled" ? groupsResult.value.backend_models : null,
51
+ log_retention_days: retentionResult.status === "fulfilled" ? retentionResult.value : null,
52
+ };
53
+ });
25
54
  app.get("/admin/api/logs", { schema: { querystring: LogQuerySchema } }, async (request, reply) => {
26
55
  const query = request.query;
27
56
  const page = parseInt(query.page || "1", 10);
@@ -8,6 +8,22 @@ export const adminMonitorRoutes = (app, options, done) => {
8
8
  done();
9
9
  return;
10
10
  }
11
+ app.get("/admin/api/monitor/init", async () => {
12
+ const [activeResult, recentResult, statsResult, concurrencyResult, runtimeResult] = await Promise.allSettled([
13
+ tracker.getActive(),
14
+ tracker.getRecent(),
15
+ tracker.getStats(),
16
+ tracker.getConcurrency(),
17
+ tracker.getRuntime(),
18
+ ]);
19
+ return {
20
+ active: activeResult.status === "fulfilled" ? activeResult.value : null,
21
+ recent: recentResult.status === "fulfilled" ? recentResult.value : null,
22
+ stats: statsResult.status === "fulfilled" ? statsResult.value : null,
23
+ concurrency: concurrencyResult.status === "fulfilled" ? concurrencyResult.value : null,
24
+ runtime: runtimeResult.status === "fulfilled" ? runtimeResult.value : null,
25
+ };
26
+ });
11
27
  app.get("/admin/api/monitor/active", async () => tracker.getActive());
12
28
  app.get("/admin/api/monitor/recent", async () => tracker.getRecent());
13
29
  app.get("/admin/api/monitor/stats", async () => tracker.getStats());
@@ -1,5 +1,6 @@
1
1
  import { FastifyPluginCallback } from "fastify";
2
2
  import Database from "better-sqlite3";
3
+ import type { Provider } from "../db/index.js";
3
4
  import type { StateRegistry } from "../core/registry.js";
4
5
  import type { AdaptiveController } from "../core/concurrency/index.js";
5
6
  import type { RequestTracker } from "../core/monitor/index.js";
@@ -11,5 +12,39 @@ interface ProviderRoutesOptions {
11
12
  adaptiveController?: AdaptiveController;
12
13
  proxyAgentFactory?: ProxyAgentFactory;
13
14
  }
15
+ /** 序列化 provider 列表,解密敏感字段、展开 models/endpoints。供多个端点复用 */
16
+ export declare function serializeProviders(db: Database.Database, providers: Provider[], encryptionKey: string, concurrencyStatus?: (id: string) => {
17
+ active: number;
18
+ queued: number;
19
+ }): {
20
+ id: string;
21
+ name: string;
22
+ api_type: "anthropic" | "openai" | "openai-responses";
23
+ base_url: string;
24
+ upstream_path: string | null;
25
+ api_key: string;
26
+ models: import("../config/model-context.js").ModelInfo[];
27
+ is_active: number;
28
+ max_concurrency: number;
29
+ queue_timeout_ms: number;
30
+ max_queue_size: number;
31
+ adaptive_enabled: number;
32
+ proxy_type: string | null;
33
+ proxy_url: string | null;
34
+ proxy_username: string | null;
35
+ proxy_password: string | null;
36
+ endpoints: {
37
+ api_type: import("../core/types.js").ApiType;
38
+ base_url: string;
39
+ upstream_path: string | null;
40
+ api_key: string;
41
+ }[];
42
+ concurrency_status: {
43
+ active: number;
44
+ queued: number;
45
+ };
46
+ created_at: string;
47
+ updated_at: string;
48
+ }[];
14
49
  export declare const adminProviderRoutes: FastifyPluginCallback<ProviderRoutesOptions>;
15
50
  export {};
@@ -1,11 +1,12 @@
1
1
  import { Type } from "@sinclair/typebox";
2
2
  import { getAllProviders, getProviderById, createProvider, updateProvider, deleteProvider, getAllMappingGroups, updateMappingGroup, PROVIDER_CONCURRENCY_DEFAULTS } from "../db/index.js";
3
3
  import { parseEndpoints, serializeEndpoints } from "../db/providers.js";
4
+ import { getRecommendedProviders } from "../config/recommended.js";
4
5
  import { encrypt, decrypt } from "../utils/crypto.js";
5
6
  import { getSetting } from "../db/settings.js";
6
7
  import { HTTP_CREATED, HTTP_NOT_FOUND, HTTP_CONFLICT, HTTP_BAD_REQUEST, HTTP_OK } from "./constants.js";
7
8
  import { API_CODE, apiError } from "./api-response.js";
8
- import { parseModels, buildModelInfoList, normalizePatchName } from "../config/model-context.js";
9
+ import { parseModels, buildModelInfoList, normalizePatchName, lookupCapabilities } from "../config/model-context.js";
9
10
  import { getModelInfoForProvider, setModelInfoForProvider, deleteAllModelInfoForProvider } from "../db/model-info.js";
10
11
  import { buildUpstreamHeaders } from "../proxy/proxy-core.js";
11
12
  import { callGet } from "../proxy/transport/http.js";
@@ -268,42 +269,62 @@ async function handleCreateProvider(body, reply, deps) {
268
269
  });
269
270
  return reply.code(HTTP_CREATED).send({ id });
270
271
  }
272
+ /** 序列化 provider 列表,解密敏感字段、展开 models/endpoints。供多个端点复用 */
273
+ export function serializeProviders(db, providers, encryptionKey, concurrencyStatus) {
274
+ return providers.map((s) => {
275
+ const modelEntries = parseModels(s.models || "[]");
276
+ const overrides = new Map(getModelInfoForProvider(db, s.id).map(m => [m.model_name, m.context_window]));
277
+ return {
278
+ id: s.id,
279
+ name: s.name,
280
+ api_type: s.api_type,
281
+ base_url: s.base_url,
282
+ upstream_path: s.upstream_path,
283
+ api_key: s.api_key ? decrypt(s.api_key, encryptionKey) : "",
284
+ models: buildModelInfoList(modelEntries, overrides),
285
+ is_active: s.is_active,
286
+ max_concurrency: s.max_concurrency,
287
+ queue_timeout_ms: s.queue_timeout_ms,
288
+ max_queue_size: s.max_queue_size,
289
+ adaptive_enabled: s.adaptive_enabled,
290
+ proxy_type: s.proxy_type,
291
+ proxy_url: s.proxy_url,
292
+ proxy_username: s.proxy_username ? decrypt(s.proxy_username, encryptionKey) : null,
293
+ proxy_password: s.proxy_password ? decrypt(s.proxy_password, encryptionKey) : null,
294
+ endpoints: parseEndpoints(s.endpoints).map(ep => ({
295
+ api_type: ep.api_type,
296
+ base_url: ep.base_url,
297
+ upstream_path: ep.upstream_path ?? null,
298
+ api_key: ep.api_key ? decrypt(ep.api_key, encryptionKey) : "",
299
+ })),
300
+ concurrency_status: concurrencyStatus?.(s.id) ?? { active: 0, queued: 0 },
301
+ created_at: s.created_at,
302
+ updated_at: s.updated_at,
303
+ };
304
+ });
305
+ }
271
306
  export const adminProviderRoutes = (app, options, done) => {
272
307
  const { db, stateRegistry, tracker, adaptiveController, proxyAgentFactory } = options;
273
308
  app.get("/admin/api/providers", async (_request, reply) => {
274
309
  const encryptionKey = getSetting(db, "encryption_key");
275
310
  const providers = getAllProviders(db);
276
- return reply.send(providers.map((s) => {
277
- const modelEntries = parseModels(s.models || "[]");
278
- const overrides = new Map(getModelInfoForProvider(db, s.id).map(m => [m.model_name, m.context_window]));
279
- return {
280
- id: s.id,
281
- name: s.name,
282
- api_type: s.api_type,
283
- base_url: s.base_url,
284
- upstream_path: s.upstream_path,
285
- api_key: s.api_key ? decrypt(s.api_key, encryptionKey) : "",
286
- models: buildModelInfoList(modelEntries, overrides),
287
- is_active: s.is_active,
288
- max_concurrency: s.max_concurrency,
289
- queue_timeout_ms: s.queue_timeout_ms,
290
- max_queue_size: s.max_queue_size,
291
- adaptive_enabled: s.adaptive_enabled,
292
- proxy_type: s.proxy_type,
293
- proxy_url: s.proxy_url,
294
- proxy_username: s.proxy_username ? decrypt(s.proxy_username, encryptionKey) : null,
295
- proxy_password: s.proxy_password ? decrypt(s.proxy_password, encryptionKey) : null,
296
- endpoints: parseEndpoints(s.endpoints).map(ep => ({
297
- api_type: ep.api_type,
298
- base_url: ep.base_url,
299
- upstream_path: ep.upstream_path ?? null,
300
- api_key: ep.api_key ? decrypt(ep.api_key, encryptionKey) : "",
301
- })),
302
- concurrency_status: stateRegistry?.getProviderStatus(s.id) ?? { active: 0, queued: 0 },
303
- created_at: s.created_at,
304
- updated_at: s.updated_at,
305
- };
306
- }));
311
+ return reply.send(serializeProviders(db, providers, encryptionKey, (id) => stateRegistry?.getProviderStatus(id) ?? { active: 0, queued: 0 }));
312
+ });
313
+ app.get("/admin/api/providers/init", async (_request, reply) => {
314
+ const encryptionKey = getSetting(db, "encryption_key");
315
+ const providers = getAllProviders(db);
316
+ const serialized = serializeProviders(db, providers, encryptionKey, (id) => stateRegistry?.getProviderStatus(id) ?? { active: 0, queued: 0 });
317
+ const recommended = getRecommendedProviders();
318
+ for (const group of recommended) {
319
+ for (const preset of group.presets) {
320
+ const capMap = {};
321
+ for (const m of preset.models) {
322
+ capMap[m] = lookupCapabilities(m);
323
+ }
324
+ preset.modelCapabilities = capMap;
325
+ }
326
+ }
327
+ return reply.send({ providers: serialized, recommended });
307
328
  });
308
329
  app.post("/admin/api/providers", { schema: { body: CreateProviderSchema } }, async (request, reply) => {
309
330
  return handleCreateProvider(request.body, reply, { db, stateRegistry, tracker, adaptiveController });
@@ -1,6 +1,8 @@
1
1
  import { Type } from "@sinclair/typebox";
2
2
  import { getSetting, setSetting } from "../db/settings.js";
3
3
  import { clearEnhancementConfigCache } from "../proxy/routing/enhancement-config.js";
4
+ import { getAllProviders } from "../db/index.js";
5
+ import { serializeProviders } from "./providers.js";
4
6
  const UpdateProxyEnhancementSchema = Type.Object({
5
7
  tool_call_loop_enabled: Type.Boolean(),
6
8
  stream_loop_enabled: Type.Boolean(),
@@ -59,5 +61,40 @@ export const adminProxyEnhancementRoutes = (app, options, done) => {
59
61
  }
60
62
  return reply.send({ success: true });
61
63
  });
64
+ app.get("/admin/api/proxy-enhancement/init", async (_request, reply) => {
65
+ // config
66
+ const raw = getSetting(db, "proxy_enhancement");
67
+ const defaults = { tool_call_loop_enabled: false, stream_loop_enabled: false, tool_round_limit_enabled: true, tool_error_logging_enabled: false };
68
+ let config = defaults;
69
+ if (raw) {
70
+ try {
71
+ const parsed = JSON.parse(raw);
72
+ config = {
73
+ tool_call_loop_enabled: parsed.tool_call_loop_enabled ?? false,
74
+ stream_loop_enabled: parsed.stream_loop_enabled ?? false,
75
+ tool_round_limit_enabled: parsed.tool_round_limit_enabled ?? true,
76
+ tool_error_logging_enabled: parsed.tool_error_logging_enabled ?? false,
77
+ };
78
+ }
79
+ catch { /* eslint-disable-line taste/no-silent-catch -- 损坏的 JSON,回退默认 */ }
80
+ }
81
+ const aiConfigRaw = getSetting(db, "ai_retry_config");
82
+ let aiRetryConfig = null;
83
+ if (aiConfigRaw) {
84
+ try {
85
+ aiRetryConfig = JSON.parse(aiConfigRaw);
86
+ }
87
+ catch (e) {
88
+ console.error('proxyEnhancementInit.parseAiConfig:', e);
89
+ aiRetryConfig = null;
90
+ }
91
+ }
92
+ const fullConfig = { ...config, ai_retry_config: aiRetryConfig };
93
+ // providers — simplified list with id, name, models for AI Retry selection
94
+ const encryptionKey = getSetting(db, "encryption_key");
95
+ const providers = getAllProviders(db);
96
+ const serializedProviders = serializeProviders(db, providers, encryptionKey);
97
+ return reply.send({ config: fullConfig, providers: serializedProviders });
98
+ });
62
99
  done();
63
100
  };
@@ -7,6 +7,10 @@ import { createRetryRule } from "../db/retry-rules.js";
7
7
  import { upsertTransformRule } from "../db/transform-rules.js";
8
8
  import { encrypt } from "../utils/crypto.js";
9
9
  import { getSetting } from "../db/settings.js";
10
+ import { getRecommendedProviders, getRecommendedRetryRules } from "../config/recommended.js";
11
+ import { lookupCapabilities } from "../config/model-context.js";
12
+ import { getAllMappingGroups, getAllProviders } from "../db/index.js";
13
+ import { serializeProviders } from "./providers.js";
10
14
  import { HTTP_CREATED, HTTP_BAD_REQUEST, HTTP_BAD_GATEWAY, HTTP_CONFLICT } from "./constants.js";
11
15
  import { API_CODE, apiError } from "./api-response.js";
12
16
  const PROVIDER_NAME_RE = /^[a-zA-Z0-9_-]+$/;
@@ -224,6 +228,35 @@ export const adminQuickSetupRoutes = (app, options, done) => {
224
228
  });
225
229
  return reply.code(HTTP_CREATED).send({ success: true, provider_id: providerId });
226
230
  });
231
+ app.get("/admin/api/quick-setup/init", async (_request, reply) => {
232
+ // provider_groups (recommended providers with capabilities)
233
+ const groups = getRecommendedProviders();
234
+ for (const group of groups) {
235
+ for (const preset of group.presets) {
236
+ const capMap = {};
237
+ for (const m of preset.models) {
238
+ capMap[m] = lookupCapabilities(m);
239
+ }
240
+ preset.modelCapabilities = capMap;
241
+ }
242
+ }
243
+ // recommended_rules (with exists flag)
244
+ const rules = getRecommendedRetryRules();
245
+ const existing = new Set(db.prepare("SELECT name FROM retry_rules").all().map((r) => r.name));
246
+ const recommendedRules = rules.map(r => ({ ...r, exists: existing.has(r.name) }));
247
+ // existing_mappings
248
+ const existingMappings = getAllMappingGroups(db);
249
+ // existing_providers
250
+ const encryptionKey = getSetting(db, "encryption_key");
251
+ const providers = getAllProviders(db);
252
+ const serializedProviders = serializeProviders(db, providers, encryptionKey);
253
+ return reply.send({
254
+ provider_groups: groups,
255
+ recommended_rules: recommendedRules,
256
+ existing_mappings: existingMappings,
257
+ existing_providers: serializedProviders,
258
+ });
259
+ });
227
260
  // ---------- Test Connection ----------
228
261
  const TestConnectionSchema = Type.Object({
229
262
  api_type: Type.Union([Type.Literal("openai"), Type.Literal("openai-responses"), Type.Literal("anthropic")]),
@@ -1,5 +1,6 @@
1
1
  import { Type } from "@sinclair/typebox";
2
- import { getAllRetryRules, getRetryRuleById, createRetryRule, updateRetryRule, deleteRetryRule, } from "../db/index.js";
2
+ import { getAllRetryRules, getRetryRuleById, createRetryRule, updateRetryRule, deleteRetryRule, getAllProviders, } from "../db/index.js";
3
+ import { getRecommendedRetryRules } from "../config/recommended.js";
3
4
  import { callLLM } from "../utils/llm-client.js";
4
5
  import { getActiveRetryRules } from "../db/retry-rules.js";
5
6
  import { getRequestLogById } from "../db/logs.js";
@@ -270,6 +271,17 @@ export const adminRetryRuleRoutes = (app, options, done) => {
270
271
  const rules = getAllRetryRules(db);
271
272
  return reply.send(rules);
272
273
  });
274
+ app.get("/admin/api/retry-rules/init", async (_request, reply) => {
275
+ const rules = getAllRetryRules(db);
276
+ const providers = getAllProviders(db).map(p => ({ id: p.id, name: p.name }));
277
+ const recommendedRaw = getRecommendedRetryRules();
278
+ const existing = new Set(db.prepare("SELECT name FROM retry_rules").all().map(r => r.name));
279
+ const recommended_rules = recommendedRaw.map(r => ({
280
+ ...r,
281
+ exists: existing.has(r.name),
282
+ }));
283
+ return reply.send({ rules, providers, recommended_rules });
284
+ });
273
285
  app.post("/admin/api/retry-rules", { schema: { body: CreateRetryRuleSchema } }, async (request, reply) => {
274
286
  const body = request.body;
275
287
  const regexError = validateBodyPattern(body.body_pattern);
@@ -89,6 +89,12 @@ export const adminRouterKeyRoutes = (app, options, done) => {
89
89
  deleteRouterKey(db, id);
90
90
  return reply.send({ success: true });
91
91
  });
92
+ app.get("/admin/api/router-keys/init", async (_request, reply) => {
93
+ const keys = getAllRouterKeys(db);
94
+ const serialized = keys.map((rk) => toPublicRouterKey(rk, db));
95
+ const availableModels = getAvailableModels(db);
96
+ return reply.send({ keys: serialized, available_models: availableModels });
97
+ });
92
98
  app.get("/admin/api/models/available", async (_request, reply) => {
93
99
  const models = getAvailableModels(db);
94
100
  return reply.send(models);
@@ -42,7 +42,7 @@ export const adminRoutes = (app, options, done) => {
42
42
  app.register(adminUsageRoutes, { db: options.db });
43
43
  app.register(adminQuickSetupRoutes, { db: options.db, stateRegistry: options.stateRegistry, tracker: options.tracker, adaptiveController: options.adaptiveController });
44
44
  app.register(adminUpgradeRoutes, { db: options.db, closeFn: options.closeFn ?? (async () => { }) });
45
- app.register(adminDashboardRoutes, { db: options.db });
45
+ app.register(adminDashboardRoutes, { db: options.db, stateRegistry: options.stateRegistry });
46
46
  app.register(adminTransformRuleRoutes, { db: options.db, pluginRegistry: options.pluginRegistry });
47
47
  // Pipeline hooks 查询
48
48
  app.get("/admin/api/pipeline/hooks", async () => {
@@ -1,6 +1,8 @@
1
1
  import { Type } from "@sinclair/typebox";
2
- import { getSchedulesByGroup, getAllSchedules, getScheduleById, createSchedule, updateSchedule, deleteSchedule, } from "../db/index.js";
2
+ import { getSchedulesByGroup, getAllSchedules, getScheduleById, createSchedule, updateSchedule, deleteSchedule, getAllMappingGroups, getAllProviders, } from "../db/index.js";
3
3
  import { getMappingGroupById, getProviderById } from "../db/index.js";
4
+ import { serializeProviders } from "./providers.js";
5
+ import { getSetting } from "../db/settings.js";
4
6
  import { HTTP_BAD_REQUEST, HTTP_CREATED, HTTP_NOT_FOUND } from "./constants.js";
5
7
  import { API_CODE, apiError } from "./api-response.js";
6
8
  const CreateScheduleSchema = Type.Object({
@@ -227,5 +229,13 @@ export const adminScheduleRoutes = (app, options, done) => {
227
229
  updateSchedule(db, id, { enabled: newEnabled });
228
230
  return reply.send({ success: true, enabled: newEnabled });
229
231
  });
232
+ app.get("/admin/api/schedules/init", async (_request, reply) => {
233
+ const schedules = getAllSchedules(db);
234
+ const mappingGroups = getAllMappingGroups(db);
235
+ const encryptionKey = getSetting(db, "encryption_key");
236
+ const providers = getAllProviders(db);
237
+ const serializedProviders = serializeProviders(db, providers, encryptionKey);
238
+ return reply.send({ schedules, mapping_groups: mappingGroups, providers: serializedProviders });
239
+ });
230
240
  done();
231
241
  };