llm-simple-router 0.9.9 → 0.9.14

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 (113) hide show
  1. package/dist/db/migrations/001_init.sql +37 -0
  2. package/dist/db/migrations/002_add_request_response_body.sql +2 -0
  3. package/dist/db/migrations/003_add_full_request_chain_log.sql +4 -0
  4. package/dist/db/migrations/004_rename_to_providers.sql +9 -0
  5. package/dist/db/migrations/005_add_api_key_preview.sql +1 -0
  6. package/dist/db/migrations/006_create_request_metrics.sql +20 -0
  7. package/dist/db/migrations/007_add_retry_fields.sql +2 -0
  8. package/dist/db/migrations/008_create_router_keys.sql +17 -0
  9. package/dist/db/migrations/009_add_request_logs_indexes.sql +2 -0
  10. package/dist/db/migrations/010_add_key_encrypted.sql +1 -0
  11. package/dist/db/migrations/011_create_mapping_groups.sql +37 -0
  12. package/dist/db/migrations/012_add_provider_models.sql +2 -0
  13. package/dist/db/migrations/013_add_retry_strategy.sql +4 -0
  14. package/dist/db/migrations/014_create_settings.sql +4 -0
  15. package/dist/db/migrations/015_add_original_model.sql +1 -0
  16. package/dist/db/migrations/016_create_session_model_tables.sql +24 -0
  17. package/dist/db/migrations/017_add_provider_concurrency.sql +3 -0
  18. package/dist/db/migrations/018_add_failover_field.sql +2 -0
  19. package/dist/db/migrations/019_create_usage_windows.sql +11 -0
  20. package/dist/db/migrations/020_drop_log_redundancy.sql +8 -0
  21. package/dist/db/migrations/021_merge_metrics_columns.sql +28 -0
  22. package/dist/db/migrations/022_add_session_id_and_incremental_vacuum.sql +5 -0
  23. package/dist/db/migrations/023_create_provider_model_info.sql +8 -0
  24. package/dist/db/migrations/024_add_mapping_groups_is_active.sql +4 -0
  25. package/dist/db/migrations/025_add_client_status_code.sql +3 -0
  26. package/dist/db/migrations/026_create_schedules_simplify_mappings.sql +64 -0
  27. package/dist/db/migrations/027_metrics_independent.sql +54 -0
  28. package/dist/db/migrations/028_ensure_strategy_column.sql +11 -0
  29. package/dist/db/migrations/029_convert_old_rule_format.sql +7 -0
  30. package/dist/db/migrations/030_add_input_tokens_estimated.sql +6 -0
  31. package/dist/db/migrations/031_add_tps_breakdown.sql +13 -0
  32. package/dist/db/migrations/032_add_non_thinking_tps.sql +3 -0
  33. package/dist/db/migrations/033_add_adaptive_concurrency.sql +6 -0
  34. package/dist/db/migrations/034_create_provider_transform_rules.sql +11 -0
  35. package/dist/db/migrations/035_drop_redundant_log_columns.sql +13 -0
  36. package/dist/db/migrations/036_add_openai_responses_api_type.sql +68 -0
  37. package/dist/db/migrations/037_fix_035_data_corruption.sql +54 -0
  38. package/dist/db/migrations/038_add_upstream_path.sql +7 -0
  39. package/frontend-dist/assets/CardContent-BTxpErx_.js +1 -0
  40. package/frontend-dist/assets/CardTitle-DuW6MHMC.js +1 -0
  41. package/frontend-dist/assets/CascadingModelSelect-CAv9_cK6.js +1 -0
  42. package/frontend-dist/assets/Checkbox-Bov0jC2d.js +1 -0
  43. package/frontend-dist/assets/CollapsibleContent-ignsLSmA.js +1 -0
  44. package/frontend-dist/assets/CollapsibleTrigger-K-O5aS5D.js +1 -0
  45. package/frontend-dist/assets/Dashboard-Byh_loe7.js +3 -0
  46. package/frontend-dist/assets/Input-DMYJg0mG.js +1 -0
  47. package/frontend-dist/assets/Label-Oc3EnENu.js +1 -0
  48. package/frontend-dist/assets/Login-CLYNuFun.js +1 -0
  49. package/frontend-dist/assets/Logs-DNDdoBjz.js +1 -0
  50. package/frontend-dist/assets/MappingList-BDJ3PU77.js +1 -0
  51. package/frontend-dist/assets/ModelCard-DwWmKLT8.js +1 -0
  52. package/frontend-dist/assets/ModelMappings-B9Kgw2uZ.js +1 -0
  53. package/frontend-dist/assets/Monitor-BJeXYuVI.js +1 -0
  54. package/frontend-dist/assets/Providers-D_kpZeYw.js +1 -0
  55. package/frontend-dist/assets/ProxyEnhancement-CC8DiK4r.js +5 -0
  56. package/frontend-dist/assets/QuickSetup-DYVI5orW.js +1 -0
  57. package/frontend-dist/assets/RetryRules-BYUok9Yi.js +1 -0
  58. package/frontend-dist/assets/RouterKeys-Byen_qRe.js +1 -0
  59. package/frontend-dist/assets/RovingFocusItem-BzUY2gsB.js +1 -0
  60. package/frontend-dist/assets/Schedules-B8GwTjC6.js +1 -0
  61. package/frontend-dist/assets/Settings-P4uUO9v9.js +6 -0
  62. package/frontend-dist/assets/Setup-CXwlz44a.js +1 -0
  63. package/frontend-dist/assets/Switch-BdEmKbpM.js +1 -0
  64. package/frontend-dist/assets/TableHeader-BwbPx6um.js +1 -0
  65. package/frontend-dist/assets/TooltipTrigger-C_zGJ_WM.js +1 -0
  66. package/frontend-dist/assets/UnifiedRequestDialog-BqPiKnUw.js +3 -0
  67. package/frontend-dist/assets/UnifiedRequestDialog-C4MTxb25.css +1 -0
  68. package/frontend-dist/assets/VisuallyHiddenInput-CaWxiN_J.js +1 -0
  69. package/frontend-dist/assets/button-tBshdyhO.js +14 -0
  70. package/frontend-dist/assets/common-BpwAv-lj.js +1 -0
  71. package/frontend-dist/assets/common-D96jEq-h.js +1 -0
  72. package/frontend-dist/assets/constants-CVHpKrRN.js +1 -0
  73. package/frontend-dist/assets/copy--LcebZO_.js +1 -0
  74. package/frontend-dist/assets/dashboard-DVDFmK36.js +1 -0
  75. package/frontend-dist/assets/dashboard-DzlE5uZS.js +1 -0
  76. package/frontend-dist/assets/dialog-CFIfggFY.js +1 -0
  77. package/frontend-dist/assets/index-Cuaqp0bp.css +1 -0
  78. package/frontend-dist/assets/index-DLWlImkm.js +3 -0
  79. package/frontend-dist/assets/login-BTNL5nN5.js +1 -0
  80. package/frontend-dist/assets/login-Sef1i0de.js +1 -0
  81. package/frontend-dist/assets/logs-B-6cgV12.js +1 -0
  82. package/frontend-dist/assets/logs-CBRLywRw.js +1 -0
  83. package/frontend-dist/assets/mappings-Cazz3EF4.js +1 -0
  84. package/frontend-dist/assets/mappings-DQRteuwa.js +1 -0
  85. package/frontend-dist/assets/monitor-Baldqd3x.js +1 -0
  86. package/frontend-dist/assets/monitor-DeInYpBf.js +1 -0
  87. package/frontend-dist/assets/providers-CxmHNt5G.js +1 -0
  88. package/frontend-dist/assets/providers-D6mXnhF_.js +1 -0
  89. package/frontend-dist/assets/proxyEnhancement-Ce12tTE4.js +1 -0
  90. package/frontend-dist/assets/proxyEnhancement-DZAnOVgK.js +1 -0
  91. package/frontend-dist/assets/quickSetup-cCofuCNs.js +1 -0
  92. package/frontend-dist/assets/quickSetup-xHkKkA6i.js +1 -0
  93. package/frontend-dist/assets/requestDetail-OsCIcS79.js +1 -0
  94. package/frontend-dist/assets/requestDetail-bH5SerEV.js +1 -0
  95. package/frontend-dist/assets/retryRules-BXrRL52J.js +1 -0
  96. package/frontend-dist/assets/retryRules-CToGC6cR.js +1 -0
  97. package/frontend-dist/assets/routerKeys-Be7OZCn0.js +1 -0
  98. package/frontend-dist/assets/routerKeys-DbTg4OP1.js +1 -0
  99. package/frontend-dist/assets/schedules-CPV0fmb-.js +1 -0
  100. package/frontend-dist/assets/schedules-DRizOKfa.js +1 -0
  101. package/frontend-dist/assets/settings-C4zZB9GY.js +1 -0
  102. package/frontend-dist/assets/settings-DCS-RTKl.js +1 -0
  103. package/frontend-dist/assets/setup-CrjgRrYP.js +1 -0
  104. package/frontend-dist/assets/setup-DmgXvgkY.js +1 -0
  105. package/frontend-dist/assets/sidebar-CLHUZFGw.js +1 -0
  106. package/frontend-dist/assets/sidebar-DwNPVJOB.js +1 -0
  107. package/frontend-dist/assets/trash-2-BUsG5SFN.js +1 -0
  108. package/frontend-dist/assets/useClipboard-DoV11ZLg.js +1 -0
  109. package/frontend-dist/assets/useLogRetention-_c8HObDe.js +1 -0
  110. package/frontend-dist/favicon.svg +1 -0
  111. package/frontend-dist/icons.svg +24 -0
  112. package/frontend-dist/index.html +15 -0
  113. package/package.json +4 -2
@@ -0,0 +1,37 @@
1
+ CREATE TABLE IF NOT EXISTS migrations (
2
+ name TEXT PRIMARY KEY,
3
+ applied_at TEXT NOT NULL
4
+ );
5
+
6
+ CREATE TABLE IF NOT EXISTS backend_services (
7
+ id TEXT PRIMARY KEY,
8
+ name TEXT NOT NULL,
9
+ api_type TEXT NOT NULL CHECK(api_type IN ('openai', 'anthropic')),
10
+ base_url TEXT NOT NULL,
11
+ api_key TEXT NOT NULL,
12
+ is_active INTEGER NOT NULL DEFAULT 1,
13
+ created_at TEXT NOT NULL,
14
+ updated_at TEXT NOT NULL
15
+ );
16
+
17
+ CREATE TABLE IF NOT EXISTS model_mappings (
18
+ id TEXT PRIMARY KEY,
19
+ client_model TEXT NOT NULL UNIQUE,
20
+ backend_model TEXT NOT NULL,
21
+ backend_service_id TEXT NOT NULL,
22
+ is_active INTEGER NOT NULL DEFAULT 1,
23
+ created_at TEXT NOT NULL,
24
+ FOREIGN KEY (backend_service_id) REFERENCES backend_services(id)
25
+ );
26
+
27
+ CREATE TABLE IF NOT EXISTS request_logs (
28
+ id TEXT PRIMARY KEY,
29
+ api_type TEXT NOT NULL,
30
+ model TEXT,
31
+ backend_service_id TEXT,
32
+ status_code INTEGER,
33
+ latency_ms INTEGER,
34
+ is_stream INTEGER,
35
+ error_message TEXT,
36
+ created_at TEXT NOT NULL
37
+ );
@@ -0,0 +1,2 @@
1
+ ALTER TABLE request_logs ADD COLUMN request_body TEXT;
2
+ ALTER TABLE request_logs ADD COLUMN response_body TEXT;
@@ -0,0 +1,4 @@
1
+ ALTER TABLE request_logs ADD COLUMN client_request TEXT;
2
+ ALTER TABLE request_logs ADD COLUMN upstream_request TEXT;
3
+ ALTER TABLE request_logs ADD COLUMN upstream_response TEXT;
4
+ ALTER TABLE request_logs ADD COLUMN client_response TEXT;
@@ -0,0 +1,9 @@
1
+ -- backend_services → providers
2
+ -- model_mappings.backend_service_id → provider_id
3
+ -- request_logs.backend_service_id → provider_id
4
+
5
+ ALTER TABLE backend_services RENAME TO providers;
6
+
7
+ ALTER TABLE model_mappings RENAME COLUMN backend_service_id TO provider_id;
8
+
9
+ ALTER TABLE request_logs RENAME COLUMN backend_service_id TO provider_id;
@@ -0,0 +1 @@
1
+ ALTER TABLE providers ADD COLUMN api_key_preview TEXT;
@@ -0,0 +1,20 @@
1
+ CREATE TABLE request_metrics (
2
+ id TEXT PRIMARY KEY,
3
+ request_log_id TEXT NOT NULL UNIQUE REFERENCES request_logs(id) ON DELETE CASCADE,
4
+ provider_id TEXT NOT NULL,
5
+ backend_model TEXT NOT NULL,
6
+ api_type TEXT NOT NULL,
7
+ input_tokens INTEGER,
8
+ output_tokens INTEGER,
9
+ cache_creation_tokens INTEGER,
10
+ cache_read_tokens INTEGER,
11
+ ttft_ms INTEGER,
12
+ total_duration_ms INTEGER,
13
+ tokens_per_second REAL,
14
+ stop_reason TEXT,
15
+ is_complete INTEGER NOT NULL DEFAULT 1,
16
+ created_at TEXT NOT NULL DEFAULT (datetime('now'))
17
+ );
18
+
19
+ CREATE INDEX idx_metrics_time_provider_model ON request_metrics(created_at, provider_id, backend_model);
20
+ CREATE INDEX idx_metrics_api_type_created_at ON request_metrics(api_type, created_at);
@@ -0,0 +1,2 @@
1
+ ALTER TABLE request_logs ADD COLUMN is_retry INTEGER NOT NULL DEFAULT 0;
2
+ ALTER TABLE request_logs ADD COLUMN original_request_id TEXT;
@@ -0,0 +1,17 @@
1
+ CREATE TABLE IF NOT EXISTS router_keys (
2
+ id TEXT PRIMARY KEY,
3
+ name TEXT NOT NULL,
4
+ key_hash TEXT NOT NULL UNIQUE,
5
+ key_prefix TEXT NOT NULL,
6
+ allowed_models TEXT,
7
+ is_active INTEGER NOT NULL DEFAULT 1,
8
+ created_at TEXT NOT NULL DEFAULT (datetime('now')),
9
+ updated_at TEXT NOT NULL DEFAULT (datetime('now'))
10
+ );
11
+
12
+ CREATE INDEX IF NOT EXISTS idx_router_keys_hash ON router_keys(key_hash);
13
+ CREATE INDEX IF NOT EXISTS idx_router_keys_active ON router_keys(is_active);
14
+
15
+ ALTER TABLE request_logs ADD COLUMN router_key_id TEXT;
16
+
17
+ CREATE INDEX IF NOT EXISTS idx_request_logs_router_key ON request_logs(router_key_id);
@@ -0,0 +1,2 @@
1
+ CREATE INDEX IF NOT EXISTS idx_request_logs_created_at ON request_logs(created_at DESC);
2
+ CREATE INDEX IF NOT EXISTS idx_request_logs_api_type ON request_logs(api_type);
@@ -0,0 +1 @@
1
+ ALTER TABLE router_keys ADD COLUMN key_encrypted TEXT;
@@ -0,0 +1,37 @@
1
+ CREATE TABLE IF NOT EXISTS mapping_groups (
2
+ id TEXT PRIMARY KEY,
3
+ client_model TEXT NOT NULL UNIQUE,
4
+ strategy TEXT NOT NULL DEFAULT 'scheduled',
5
+ rule TEXT NOT NULL,
6
+ is_active INTEGER NOT NULL DEFAULT 1,
7
+ created_at TEXT NOT NULL
8
+ );
9
+
10
+ CREATE TABLE IF NOT EXISTS retry_rules (
11
+ id TEXT PRIMARY KEY,
12
+ name TEXT NOT NULL,
13
+ status_code INTEGER NOT NULL,
14
+ body_pattern TEXT NOT NULL,
15
+ is_active INTEGER NOT NULL DEFAULT 1,
16
+ created_at TEXT NOT NULL
17
+ );
18
+
19
+ -- 从现有 model_mappings 迁移数据:
20
+ -- 每条旧映射转为 scheduled 策略(含 is_active 状态)
21
+ -- default 指向原后端,windows 为空数组
22
+ INSERT INTO mapping_groups (id, client_model, strategy, rule, is_active, created_at)
23
+ SELECT
24
+ lower(hex(randomblob(16))) AS id,
25
+ client_model,
26
+ 'scheduled' AS strategy,
27
+ json_object(
28
+ 'default', json_object('provider_id', provider_id, 'backend_model', backend_model),
29
+ 'windows', json_array()
30
+ ) AS rule,
31
+ is_active,
32
+ created_at
33
+ FROM model_mappings;
34
+
35
+ -- 默认重试规则在首次启动时通过 seedDefaultRules() 自动插入,不在此处硬编码
36
+
37
+ -- 旧表 model_mappings 保留为空表(兼容现有测试),读写已全部迁移到 mapping_groups
@@ -0,0 +1,2 @@
1
+ -- 为供应商增加可用模型列表,存储 JSON 数组
2
+ ALTER TABLE providers ADD COLUMN models TEXT NOT NULL DEFAULT '[]';
@@ -0,0 +1,4 @@
1
+ ALTER TABLE retry_rules ADD COLUMN retry_strategy TEXT NOT NULL DEFAULT 'exponential';
2
+ ALTER TABLE retry_rules ADD COLUMN retry_delay_ms INTEGER NOT NULL DEFAULT 5000;
3
+ ALTER TABLE retry_rules ADD COLUMN max_retries INTEGER NOT NULL DEFAULT 10;
4
+ ALTER TABLE retry_rules ADD COLUMN max_delay_ms INTEGER NOT NULL DEFAULT 60000;
@@ -0,0 +1,4 @@
1
+ CREATE TABLE IF NOT EXISTS settings (
2
+ key TEXT PRIMARY KEY,
3
+ value TEXT NOT NULL
4
+ );
@@ -0,0 +1 @@
1
+ ALTER TABLE request_logs ADD COLUMN original_model TEXT;
@@ -0,0 +1,24 @@
1
+ CREATE TABLE IF NOT EXISTS session_model_states (
2
+ id TEXT PRIMARY KEY,
3
+ router_key_id TEXT NOT NULL,
4
+ session_id TEXT NOT NULL,
5
+ current_model TEXT NOT NULL,
6
+ original_model TEXT,
7
+ last_active_at TEXT NOT NULL,
8
+ created_at TEXT NOT NULL,
9
+ UNIQUE(router_key_id, session_id),
10
+ FOREIGN KEY (router_key_id) REFERENCES router_keys(id)
11
+ );
12
+ CREATE INDEX idx_sms_router_key ON session_model_states(router_key_id);
13
+
14
+ CREATE TABLE IF NOT EXISTS session_model_history (
15
+ id TEXT PRIMARY KEY,
16
+ router_key_id TEXT NOT NULL,
17
+ session_id TEXT NOT NULL,
18
+ old_model TEXT,
19
+ new_model TEXT NOT NULL,
20
+ trigger_type TEXT NOT NULL,
21
+ created_at TEXT NOT NULL,
22
+ FOREIGN KEY (router_key_id) REFERENCES router_keys(id)
23
+ );
24
+ CREATE INDEX idx_smh_session ON session_model_history(router_key_id, session_id);
@@ -0,0 +1,3 @@
1
+ ALTER TABLE providers ADD COLUMN max_concurrency INTEGER NOT NULL DEFAULT 0;
2
+ ALTER TABLE providers ADD COLUMN queue_timeout_ms INTEGER NOT NULL DEFAULT 0;
3
+ ALTER TABLE providers ADD COLUMN max_queue_size INTEGER NOT NULL DEFAULT 100;
@@ -0,0 +1,2 @@
1
+ ALTER TABLE request_logs ADD COLUMN is_failover INTEGER NOT NULL DEFAULT 0;
2
+ CREATE INDEX IF NOT EXISTS idx_request_logs_original_request_id ON request_logs(original_request_id);
@@ -0,0 +1,11 @@
1
+ CREATE TABLE IF NOT EXISTS usage_windows (
2
+ id TEXT PRIMARY KEY,
3
+ router_key_id TEXT,
4
+ start_time TEXT NOT NULL,
5
+ end_time TEXT NOT NULL,
6
+ created_at TEXT DEFAULT (datetime('now'))
7
+ );
8
+
9
+ CREATE INDEX IF NOT EXISTS idx_usage_windows_start ON usage_windows(start_time);
10
+ CREATE INDEX IF NOT EXISTS idx_usage_windows_router_key ON usage_windows(router_key_id);
11
+ CREATE INDEX IF NOT EXISTS idx_usage_windows_end_time ON usage_windows(end_time);
@@ -0,0 +1,8 @@
1
+ -- 019_drop_log_redundancy.sql
2
+ -- 删除冗余大文本字段:request_body = client_request.body, response_body = upstream_response.body, client_response ≈ upstream_response
3
+ ALTER TABLE request_logs DROP COLUMN request_body;
4
+ ALTER TABLE request_logs DROP COLUMN response_body;
5
+ ALTER TABLE request_logs DROP COLUMN client_response;
6
+
7
+ -- 日志自动清理保留天数(默认 3 天,0 = 不自动清理)
8
+ INSERT OR IGNORE INTO settings (key, value) VALUES ('log_retention_days', '3');
@@ -0,0 +1,28 @@
1
+ -- V1 双写:将 request_metrics 列冗余到 request_logs,消除日志查询的 JOIN
2
+ -- request_metrics 表保留不动,聚合查询仍查它
3
+
4
+ ALTER TABLE request_logs ADD COLUMN input_tokens INTEGER;
5
+ ALTER TABLE request_logs ADD COLUMN output_tokens INTEGER;
6
+ ALTER TABLE request_logs ADD COLUMN cache_read_tokens INTEGER;
7
+ ALTER TABLE request_logs ADD COLUMN ttft_ms INTEGER;
8
+ ALTER TABLE request_logs ADD COLUMN tokens_per_second REAL;
9
+ ALTER TABLE request_logs ADD COLUMN stop_reason TEXT;
10
+ ALTER TABLE request_logs ADD COLUMN backend_model TEXT;
11
+ ALTER TABLE request_logs ADD COLUMN metrics_complete INTEGER NOT NULL DEFAULT 0;
12
+
13
+ -- 流式请求的累积文本内容(从 tracker.appendStreamChunk 中提取的纯文本)
14
+ ALTER TABLE request_logs ADD COLUMN stream_text_content TEXT;
15
+
16
+ -- 回填历史数据:把已有的 request_metrics 写入 request_logs 新列
17
+ UPDATE request_logs
18
+ SET
19
+ input_tokens = rm.input_tokens,
20
+ output_tokens = rm.output_tokens,
21
+ cache_read_tokens = rm.cache_read_tokens,
22
+ ttft_ms = rm.ttft_ms,
23
+ tokens_per_second = rm.tokens_per_second,
24
+ stop_reason = rm.stop_reason,
25
+ backend_model = rm.backend_model,
26
+ metrics_complete = rm.is_complete
27
+ FROM request_metrics rm
28
+ WHERE rm.request_log_id = request_logs.id;
@@ -0,0 +1,5 @@
1
+ ALTER TABLE request_logs ADD COLUMN session_id TEXT;
2
+
3
+ -- NOTE: PRAGMA auto_vacuum only takes effect after VACUUM for existing databases.
4
+ -- Run scripts/vacuum-migrate.js for existing deployments.
5
+ PRAGMA auto_vacuum = 2;
@@ -0,0 +1,8 @@
1
+ -- 为每个 provider 的每个 model 单独存储 context_window
2
+ CREATE TABLE IF NOT EXISTS provider_model_info (
3
+ provider_id TEXT NOT NULL,
4
+ model_name TEXT NOT NULL,
5
+ context_window INTEGER NOT NULL,
6
+ PRIMARY KEY (provider_id, model_name),
7
+ FOREIGN KEY (provider_id) REFERENCES providers(id) ON DELETE CASCADE
8
+ );
@@ -0,0 +1,4 @@
1
+ -- is_active 列已在 migration 011 中创建。此处保留作为幂等安全网:
2
+ -- 若 DB 来自旧版(011 未含 is_active),此 ALTER 会补充该列。
3
+ -- 若已存在则触发 "duplicate column name" 错误,由 migration runner 捕获忽略。
4
+ ALTER TABLE mapping_groups ADD COLUMN is_active INTEGER NOT NULL DEFAULT 1;
@@ -0,0 +1,3 @@
1
+ -- 记录 router 实际发送给客户端的 HTTP status code
2
+ -- 当与 status_code(上游原始值)不同时有值,用于追溯 status 转换
3
+ ALTER TABLE request_logs ADD COLUMN client_status_code INTEGER;
@@ -0,0 +1,64 @@
1
+ -- Migration 026: 创建 schedules 表,简化 mapping_groups rule 格式
2
+ -- 变更概要:
3
+ -- 1. 创建 schedules 表(周循环调度层)
4
+ -- 2. 迁移 scheduled 策略的 windows 数据到 schedules
5
+ -- 3. 统一 mapping_groups.rule 为 { targets: [...] } 格式
6
+ -- 4. 保留 strategy 列(兼容旧代码),默认 'scheduled'
7
+
8
+ -- ============================================================
9
+ -- Step 1: 创建 schedules 表
10
+ -- ============================================================
11
+ CREATE TABLE IF NOT EXISTS schedules (
12
+ id TEXT PRIMARY KEY,
13
+ mapping_group_id TEXT NOT NULL,
14
+ name TEXT NOT NULL,
15
+ enabled INTEGER NOT NULL DEFAULT 1,
16
+ week TEXT NOT NULL DEFAULT '[]',
17
+ start_hour INTEGER NOT NULL DEFAULT 0,
18
+ end_hour INTEGER NOT NULL DEFAULT 24,
19
+ mapping_rule TEXT NOT NULL DEFAULT '{}',
20
+ concurrency_rule TEXT,
21
+ priority INTEGER NOT NULL DEFAULT 0,
22
+ created_at TEXT NOT NULL,
23
+ updated_at TEXT NOT NULL,
24
+ FOREIGN KEY (mapping_group_id) REFERENCES mapping_groups(id) ON DELETE CASCADE
25
+ );
26
+
27
+ CREATE INDEX IF NOT EXISTS idx_schedules_group ON schedules(mapping_group_id);
28
+ CREATE INDEX IF NOT EXISTS idx_schedules_enabled ON schedules(enabled);
29
+
30
+ -- ============================================================
31
+ -- Step 2: 迁移 scheduled 策略的 windows 数据
32
+ -- ============================================================
33
+ -- 从 strategy='scheduled' 的 mapping_groups 中提取 windows 数组,
34
+ -- 每个 window 生成一条 schedule 记录。
35
+ -- rule 格式: { "default": { "provider_id": "...", "backend_model": "..." },
36
+ -- "windows": [{ "start": "09:00", "end": "18:00",
37
+ -- "target": { "provider_id": "...", "backend_model": "..." } }] }
38
+ -- json_each 展开数组时,key 是数组索引(从 0 开始),value 是元素本身
39
+ INSERT INTO schedules (id, mapping_group_id, name, enabled, week, start_hour, end_hour,
40
+ mapping_rule, priority, created_at, updated_at)
41
+ SELECT
42
+ lower(hex(randomblob(16))) AS id,
43
+ mg.id AS mapping_group_id,
44
+ 'Schedule ' || (win.key + 1) AS name,
45
+ 1 AS enabled,
46
+ '[0,1,2,3,4,5,6]' AS week,
47
+ -- "09:00" -> 9, "18:30" -> 18(CAST 会自动截取整数部分)
48
+ CAST(json_extract(win.value, '$.start') AS INTEGER) AS start_hour,
49
+ CAST(json_extract(win.value, '$.end') AS INTEGER) AS end_hour,
50
+ json_object('targets', json_array(json_extract(win.value, '$.target'))) AS mapping_rule,
51
+ 0 AS priority,
52
+ mg.created_at,
53
+ mg.created_at AS updated_at
54
+ FROM mapping_groups mg, json_each(json_extract(mg.rule, '$.windows')) AS win
55
+ WHERE mg.strategy = 'scheduled';
56
+
57
+ -- ============================================================
58
+ -- Step 3: 更新 rule 格式(in-place,不重建表以保留 strategy 列)
59
+ -- ============================================================
60
+ -- scheduled: rule 从 { default, windows } 简化为 { targets: [default] }
61
+ -- failover/round-robin/random: rule 已经是 { targets: [...] },保持不变
62
+ -- 安全过滤:只处理仍有 $.default 的旧格式,避免覆盖已存在的 {targets} 格式
63
+ UPDATE mapping_groups SET rule = json_object('targets', json_array(json_extract(rule, '$.default')))
64
+ WHERE strategy = 'scheduled' AND json_extract(rule, '$.default') IS NOT NULL;
@@ -0,0 +1,54 @@
1
+ -- Migration 027 (原 026 metrics): Metrics 独立化:request_metrics 增加路由维度列,解除级联删除依赖
2
+ -- usage_windows 增加 provider_id 支持按 provider 维度追踪使用量
3
+
4
+ -- 1. 重建 request_metrics:CASCADE -> SET NULL,同时新增 router_key_id / status_code
5
+ CREATE TABLE request_metrics_new (
6
+ id TEXT PRIMARY KEY,
7
+ request_log_id TEXT UNIQUE REFERENCES request_logs(id) ON DELETE SET NULL,
8
+ provider_id TEXT NOT NULL,
9
+ backend_model TEXT NOT NULL,
10
+ api_type TEXT NOT NULL,
11
+ input_tokens INTEGER,
12
+ output_tokens INTEGER,
13
+ cache_creation_tokens INTEGER,
14
+ cache_read_tokens INTEGER,
15
+ ttft_ms INTEGER,
16
+ total_duration_ms INTEGER,
17
+ tokens_per_second REAL,
18
+ stop_reason TEXT,
19
+ is_complete INTEGER NOT NULL DEFAULT 1,
20
+ router_key_id TEXT,
21
+ status_code INTEGER,
22
+ created_at TEXT NOT NULL DEFAULT (datetime('now'))
23
+ );
24
+
25
+ INSERT INTO request_metrics_new
26
+ (id, request_log_id, provider_id, backend_model, api_type,
27
+ input_tokens, output_tokens, cache_creation_tokens, cache_read_tokens,
28
+ ttft_ms, total_duration_ms, tokens_per_second, stop_reason,
29
+ is_complete, created_at)
30
+ SELECT
31
+ id, request_log_id, provider_id, backend_model, api_type,
32
+ input_tokens, output_tokens, cache_creation_tokens, cache_read_tokens,
33
+ ttft_ms, total_duration_ms, tokens_per_second, stop_reason,
34
+ is_complete, created_at
35
+ FROM request_metrics;
36
+
37
+ -- 回填 router_key_id 和 status_code 从 request_logs
38
+ UPDATE request_metrics_new
39
+ SET
40
+ router_key_id = rl.router_key_id,
41
+ status_code = rl.status_code
42
+ FROM request_logs rl
43
+ WHERE rl.id = request_metrics_new.request_log_id;
44
+
45
+ DROP TABLE request_metrics;
46
+ ALTER TABLE request_metrics_new RENAME TO request_metrics;
47
+
48
+ -- 重建原有索引
49
+ CREATE INDEX idx_metrics_time_provider_model ON request_metrics(created_at, provider_id, backend_model);
50
+ CREATE INDEX idx_metrics_api_type_created_at ON request_metrics(api_type, created_at);
51
+
52
+ -- 2. usage_windows 增加 provider_id 列
53
+ ALTER TABLE usage_windows ADD COLUMN provider_id TEXT;
54
+ CREATE INDEX IF NOT EXISTS idx_usage_windows_provider_id ON usage_windows(provider_id);
@@ -0,0 +1,11 @@
1
+ -- Migration 027: 确保 mapping_groups 表存在 strategy 列(兼容旧代码)
2
+ -- 背景:旧版 026 曾重建表删除了 strategy 列,此迁移在缺失时加回,默认 'scheduled'
3
+ --
4
+ -- 安全策略:先测试列是否存在,不存在才添加。
5
+ -- SQLite 不支持条件 DDL,但支持在单个事务中根据 SELECT 结果执行不同操作。
6
+ -- 方案:创建临时表作为标记,根据 PRAGMA 结果决定是否执行 ALTER TABLE。
7
+
8
+ -- 使用触发器 + 临时表的 trick 不可行。改用最简单的方案:
9
+ -- 如果 strategy 列已存在,ALTER TABLE 会报 "duplicate column name" 错误。
10
+ -- migration runner 会捕获这个特定错误,忽略并继续。
11
+ ALTER TABLE mapping_groups ADD COLUMN strategy TEXT NOT NULL DEFAULT 'scheduled';
@@ -0,0 +1,7 @@
1
+ -- Migration 028: 将 mapping_groups 中残留旧格式 rule 统一转为 { targets } 格式
2
+ -- 背景:旧版 026 曾重建表但未执行 rule 转换,导致部分 entry 仍为 { default, windows } 旧格式。
3
+ -- 此迁移补做 026 Step 3 的转换逻辑。
4
+
5
+ UPDATE mapping_groups
6
+ SET rule = json_object('targets', json_array(json_extract(rule, '$.default')))
7
+ WHERE json_extract(rule, '$.default') IS NOT NULL AND json_extract(rule, '$.targets') IS NULL;
@@ -0,0 +1,6 @@
1
+ -- request_metrics / request_logs 增加 input_tokens_estimated 标记,
2
+ -- 当 API 未返回 usage.input_tokens 时,后端用 gpt-tokenizer 估算并标记此字段为 1。
3
+ -- 前端据此展示 "Est Input Tokens"。
4
+
5
+ ALTER TABLE request_metrics ADD COLUMN input_tokens_estimated INTEGER NOT NULL DEFAULT 0;
6
+ ALTER TABLE request_logs ADD COLUMN input_tokens_estimated INTEGER NOT NULL DEFAULT 0;
@@ -0,0 +1,13 @@
1
+ -- 030: TPS 四指标拆分
2
+ -- 原始数据列(tokenizer 计数 + 各阶段耗时)
3
+ ALTER TABLE request_metrics ADD COLUMN thinking_tokens INTEGER;
4
+ ALTER TABLE request_metrics ADD COLUMN text_tokens INTEGER;
5
+ ALTER TABLE request_metrics ADD COLUMN tool_use_tokens INTEGER;
6
+ ALTER TABLE request_metrics ADD COLUMN thinking_duration_ms INTEGER;
7
+ ALTER TABLE request_metrics ADD COLUMN text_duration_ms INTEGER;
8
+ ALTER TABLE request_metrics ADD COLUMN tool_use_duration_ms INTEGER;
9
+ -- 计算结果列
10
+ ALTER TABLE request_metrics ADD COLUMN thinking_tps REAL;
11
+ ALTER TABLE request_metrics ADD COLUMN text_tps REAL;
12
+ ALTER TABLE request_metrics ADD COLUMN tool_use_tps REAL;
13
+ ALTER TABLE request_metrics ADD COLUMN total_tps REAL;
@@ -0,0 +1,3 @@
1
+ -- Simplify TPS model: thinking + non-thinking (instead of thinking/text/tool_use)
2
+ ALTER TABLE request_metrics ADD COLUMN non_thinking_duration_ms INTEGER;
3
+ ALTER TABLE request_metrics ADD COLUMN non_thinking_tps REAL;
@@ -0,0 +1,6 @@
1
+ -- 033_add_adaptive_concurrency.sql
2
+ ALTER TABLE providers ADD COLUMN adaptive_enabled INTEGER NOT NULL DEFAULT 0;
3
+ ALTER TABLE providers ADD COLUMN adaptive_min INTEGER NOT NULL DEFAULT 1;
4
+
5
+ -- (merged from 033_add_pipeline_snapshot)
6
+ ALTER TABLE request_logs ADD COLUMN pipeline_snapshot TEXT;
@@ -0,0 +1,11 @@
1
+ CREATE TABLE IF NOT EXISTS provider_transform_rules (
2
+ provider_id TEXT PRIMARY KEY REFERENCES providers(id) ON DELETE CASCADE,
3
+ inject_headers TEXT,
4
+ request_defaults TEXT,
5
+ drop_fields TEXT,
6
+ field_overrides TEXT,
7
+ plugin_name TEXT,
8
+ is_active INTEGER DEFAULT 1,
9
+ created_at TEXT DEFAULT (datetime('now')),
10
+ updated_at TEXT DEFAULT (datetime('now'))
11
+ );
@@ -0,0 +1,13 @@
1
+ -- 034_drop_redundant_log_columns.sql
2
+ -- request_logs 与 request_metrics 双写冗余清理:
3
+ -- metrics 字段统一由 request_metrics 承载,日志列表查询改用 LEFT JOIN。
4
+
5
+ ALTER TABLE request_logs DROP COLUMN input_tokens;
6
+ ALTER TABLE request_logs DROP COLUMN output_tokens;
7
+ ALTER TABLE request_logs DROP COLUMN cache_read_tokens;
8
+ ALTER TABLE request_logs DROP COLUMN ttft_ms;
9
+ ALTER TABLE request_logs DROP COLUMN tokens_per_second;
10
+ ALTER TABLE request_logs DROP COLUMN stop_reason;
11
+ ALTER TABLE request_logs DROP COLUMN backend_model;
12
+ ALTER TABLE request_logs DROP COLUMN metrics_complete;
13
+ ALTER TABLE request_logs DROP COLUMN input_tokens_estimated;
@@ -0,0 +1,68 @@
1
+ -- Expand api_type CHECK constraint to include 'openai-responses'
2
+ -- SQLite doesn't support ALTER TABLE ... ALTER CONSTRAINT, so we recreate the table.
3
+ -- We must temporarily drop referencing foreign key tables and recreate them after.
4
+
5
+ -- Note: This migration runs inside db.transaction() in the migration runner,
6
+ -- so we don't need our own BEGIN/COMMIT. PRAGMA foreign_keys doesn't work
7
+ -- inside transactions, so we handle FK tables explicitly instead.
8
+
9
+ -- Step 1: Save referencing table data as temp tables
10
+ CREATE TABLE IF NOT EXISTS _tmp_provider_model_info AS SELECT * FROM provider_model_info;
11
+ CREATE TABLE IF NOT EXISTS _tmp_provider_transform_rules AS SELECT * FROM provider_transform_rules;
12
+
13
+ -- Step 2: Drop referencing tables
14
+ DROP TABLE IF EXISTS provider_model_info;
15
+ DROP TABLE IF EXISTS provider_transform_rules;
16
+
17
+ -- Step 3: Recreate providers with expanded CHECK
18
+ CREATE TABLE providers_new (
19
+ id TEXT PRIMARY KEY,
20
+ name TEXT NOT NULL UNIQUE,
21
+ api_type TEXT NOT NULL CHECK(api_type IN ('openai', 'openai-responses', 'anthropic')),
22
+ base_url TEXT NOT NULL,
23
+ api_key TEXT NOT NULL,
24
+ api_key_preview TEXT,
25
+ models TEXT NOT NULL DEFAULT '[]',
26
+ is_active INTEGER NOT NULL DEFAULT 1,
27
+ max_concurrency INTEGER NOT NULL DEFAULT 0,
28
+ queue_timeout_ms INTEGER NOT NULL DEFAULT 0,
29
+ max_queue_size INTEGER NOT NULL DEFAULT 100,
30
+ adaptive_enabled INTEGER NOT NULL DEFAULT 0,
31
+ adaptive_min INTEGER NOT NULL DEFAULT 1,
32
+ created_at TEXT NOT NULL,
33
+ updated_at TEXT NOT NULL
34
+ );
35
+
36
+ INSERT INTO providers_new (id, name, api_type, base_url, api_key, api_key_preview, models, is_active, max_concurrency, queue_timeout_ms, max_queue_size, adaptive_enabled, adaptive_min, created_at, updated_at)
37
+ SELECT id, name, api_type, base_url, api_key, api_key_preview, models, is_active, max_concurrency, queue_timeout_ms, max_queue_size, adaptive_enabled, adaptive_min, created_at, updated_at FROM providers;
38
+ DROP TABLE providers;
39
+ ALTER TABLE providers_new RENAME TO providers;
40
+
41
+ -- Step 4: Recreate referencing tables with their original schemas
42
+ CREATE TABLE provider_model_info (
43
+ provider_id TEXT NOT NULL,
44
+ model_name TEXT NOT NULL,
45
+ context_window INTEGER NOT NULL,
46
+ PRIMARY KEY (provider_id, model_name),
47
+ FOREIGN KEY (provider_id) REFERENCES providers(id) ON DELETE CASCADE
48
+ );
49
+
50
+ CREATE TABLE IF NOT EXISTS provider_transform_rules (
51
+ provider_id TEXT PRIMARY KEY REFERENCES providers(id) ON DELETE CASCADE,
52
+ inject_headers TEXT,
53
+ request_defaults TEXT,
54
+ drop_fields TEXT,
55
+ field_overrides TEXT,
56
+ plugin_name TEXT,
57
+ is_active INTEGER DEFAULT 1,
58
+ created_at TEXT DEFAULT (datetime('now')),
59
+ updated_at TEXT DEFAULT (datetime('now'))
60
+ );
61
+
62
+ -- Step 5: Restore data
63
+ INSERT INTO provider_model_info SELECT * FROM _tmp_provider_model_info;
64
+ INSERT OR IGNORE INTO provider_transform_rules SELECT * FROM _tmp_provider_transform_rules;
65
+
66
+ -- Step 6: Cleanup
67
+ DROP TABLE _tmp_provider_model_info;
68
+ DROP TABLE _tmp_provider_transform_rules;
@@ -0,0 +1,54 @@
1
+ -- Fix data corruption caused by migration 036.
2
+ -- Migration 035 used `INSERT INTO providers_new SELECT * FROM providers`
3
+ -- which matches columns by position, not by name. The new table had a different
4
+ -- column order than the old table (where columns were added sequentially via
5
+ -- ALTER TABLE ADD COLUMN). This shifted every column from position 6 onward.
6
+ --
7
+ -- Old column order (via ALTER TABLE ADD COLUMN):
8
+ -- id, name, api_type, base_url, api_key, is_active, created_at, updated_at,
9
+ -- api_key_preview, models, max_concurrency, queue_timeout_ms, max_queue_size,
10
+ -- adaptive_enabled, adaptive_min
11
+ --
12
+ -- New column order (035):
13
+ -- id, name, api_type, base_url, api_key, api_key_preview, models, is_active,
14
+ -- max_concurrency, queue_timeout_ms, max_queue_size, adaptive_enabled,
15
+ -- adaptive_min, created_at, updated_at
16
+ --
17
+ -- Positional mapping of what actually went where:
18
+ -- old(6) is_active → new api_key_preview
19
+ -- old(7) created_at → new models
20
+ -- old(8) updated_at → new is_active
21
+ -- old(9) api_key_preview → new max_concurrency ← visible bug
22
+ -- old(10) models → new queue_timeout_ms
23
+ -- old(11) max_concurrency → new max_queue_size
24
+ -- old(12) queue_timeout_ms → new adaptive_enabled
25
+ -- old(13) max_queue_size → new adaptive_min
26
+ -- old(14) adaptive_enabled → new created_at
27
+ -- old(15) adaptive_min → new updated_at
28
+ --
29
+ -- Guard: only fixes rows where max_concurrency contains text data
30
+ -- (api_key_preview leaked into an INTEGER column). Providers created after
31
+ -- 035 have correct INTEGER values and are not affected.
32
+
33
+ -- Step 1: Snapshot current data before fixing
34
+ CREATE TABLE _m036_snapshot AS SELECT rowid, * FROM providers;
35
+
36
+ -- Step 2: Only fix rows where max_concurrency is text (corrupted by api_key_preview).
37
+ -- Each column reads from the snapshot position where the OLD value actually ended up.
38
+ UPDATE providers SET
39
+ api_key_preview = (SELECT max_concurrency FROM _m036_snapshot s WHERE s.rowid = providers.rowid),
40
+ models = (SELECT queue_timeout_ms FROM _m036_snapshot s WHERE s.rowid = providers.rowid),
41
+ is_active = (SELECT CAST(api_key_preview AS INTEGER) FROM _m036_snapshot s WHERE s.rowid = providers.rowid),
42
+ max_concurrency = (SELECT CAST(max_queue_size AS INTEGER) FROM _m036_snapshot s WHERE s.rowid = providers.rowid),
43
+ queue_timeout_ms = (SELECT CAST(adaptive_enabled AS INTEGER) FROM _m036_snapshot s WHERE s.rowid = providers.rowid),
44
+ max_queue_size = (SELECT CAST(adaptive_min AS INTEGER) FROM _m036_snapshot s WHERE s.rowid = providers.rowid),
45
+ adaptive_enabled = (SELECT CAST(created_at AS INTEGER) FROM _m036_snapshot s WHERE s.rowid = providers.rowid),
46
+ adaptive_min = (SELECT CAST(updated_at AS INTEGER) FROM _m036_snapshot s WHERE s.rowid = providers.rowid),
47
+ created_at = (SELECT models FROM _m036_snapshot s WHERE s.rowid = providers.rowid),
48
+ updated_at = (SELECT is_active FROM _m036_snapshot s WHERE s.rowid = providers.rowid)
49
+ WHERE typeof((
50
+ SELECT max_concurrency FROM _m036_snapshot s WHERE s.rowid = providers.rowid
51
+ )) = 'text';
52
+
53
+ -- Step 3: Cleanup
54
+ DROP TABLE _m036_snapshot;
@@ -0,0 +1,7 @@
1
+ -- Add upstream_path column to providers table.
2
+ -- When NULL, the router uses the default path based on api_type:
3
+ -- openai / openai-responses → /v1/chat/completions or /v1/responses
4
+ -- anthropic → /v1/messages
5
+ -- When set, this value overrides the default upstream path.
6
+
7
+ ALTER TABLE providers ADD COLUMN upstream_path TEXT DEFAULT NULL;
@@ -0,0 +1 @@
1
+ import{Bt as e,Q as t,Rt as n,Y as r,pt as i,r as a,ut as o}from"./button-tBshdyhO.js";var s=[`data-size`],c=t({__name:`Card`,props:{class:{type:[Boolean,null,String,Object,Array]},size:{default:`default`}},setup(t){let c=t;return(l,u)=>(o(),r(`div`,{"data-slot":`card`,"data-size":t.size,class:e(n(a)(`ring-foreground/10 bg-card text-card-foreground gap-4 overflow-hidden rounded-lg py-4 text-sm ring-1 has-data-[slot=card-footer]:pb-0 has-[>img:first-child]:pt-0 data-[size=sm]:gap-3 data-[size=sm]:py-3 data-[size=sm]:has-data-[slot=card-footer]:pb-0 *:[img:first-child]:rounded-t-lg *:[img:last-child]:rounded-b-lg group/card flex flex-col`,c.class))},[i(l.$slots,`default`)],10,s))}}),l=t({__name:`CardContent`,props:{class:{type:[Boolean,null,String,Object,Array]}},setup(t){let s=t;return(t,c)=>(o(),r(`div`,{"data-slot":`card-content`,class:e(n(a)(`px-4 group-data-[size=sm]/card:px-3`,s.class))},[i(t.$slots,`default`)],2))}});export{c as n,l as t};
@@ -0,0 +1 @@
1
+ import{Bt as e,Q as t,Rt as n,Y as r,pt as i,r as a,ut as o}from"./button-tBshdyhO.js";var s=t({__name:`CardHeader`,props:{class:{type:[Boolean,null,String,Object,Array]}},setup(t){let s=t;return(t,c)=>(o(),r(`div`,{"data-slot":`card-header`,class:e(n(a)(`gap-1 rounded-t-xl px-4 group-data-[size=sm]/card:px-3 [.border-b]:pb-4 group-data-[size=sm]/card:[.border-b]:pb-3 group/card-header @container/card-header grid auto-rows-min items-start has-data-[slot=card-action]:grid-cols-[1fr_auto] has-data-[slot=card-description]:grid-rows-[auto_auto]`,s.class))},[i(t.$slots,`default`)],2))}}),c=t({__name:`CardTitle`,props:{class:{type:[Boolean,null,String,Object,Array]}},setup(t){let s=t;return(t,c)=>(o(),r(`div`,{"data-slot":`card-title`,class:e(n(a)(`text-base leading-snug font-medium group-data-[size=sm]/card:text-sm cn-font-heading`,s.class))},[i(t.$slots,`default`)],2))}});export{s as n,c as t};