llm-simple-router 0.9.0 → 0.9.5
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.
- package/README.en.md +319 -0
- package/README.md +2 -0
- package/config/version.json +4 -0
- package/dist/admin/quick-setup.js +1 -1
- package/dist/admin/upgrade.js +8 -1
- package/dist/config/recommended.d.ts +7 -1
- package/dist/config/recommended.js +12 -0
- package/dist/core/constants.js +2 -0
- package/dist/db/index.js +5 -0
- package/dist/db/migrations/033_add_adaptive_concurrency.sql +3 -0
- package/dist/db/migrations/036_add_openai_responses_api_type.sql +68 -0
- package/dist/db/migrations/037_fix_035_data_corruption.sql +54 -0
- package/dist/db/providers.d.ts +3 -3
- package/dist/index.js +7 -3
- package/dist/metrics/metrics-extractor.d.ts +3 -2
- package/dist/metrics/metrics-extractor.js +45 -0
- package/dist/metrics/sse-metrics-transform.d.ts +1 -1
- package/dist/metrics/sse-metrics-transform.js +10 -0
- package/dist/monitor/request-tracker.d.ts +1 -1
- package/dist/monitor/stream-content-accumulator.d.ts +1 -1
- package/dist/monitor/stream-extractor.d.ts +1 -1
- package/dist/monitor/stream-extractor.js +21 -0
- package/dist/monitor/types.d.ts +1 -1
- package/dist/proxy/handler/proxy-handler-utils.d.ts +1 -1
- package/dist/proxy/handler/proxy-handler.d.ts +1 -1
- package/dist/proxy/handler/responses.d.ts +7 -0
- package/dist/proxy/handler/responses.js +48 -0
- package/dist/proxy/loop-prevention/tool-loop-guard.d.ts +1 -1
- package/dist/proxy/loop-prevention/tool-loop-guard.js +10 -0
- package/dist/proxy/orchestration/orchestrator.d.ts +1 -1
- package/dist/proxy/orchestration/semaphore.js +6 -0
- package/dist/proxy/patch/deepseek/index.d.ts +1 -1
- package/dist/proxy/patch/deepseek/patch-thinking-param.d.ts +1 -1
- package/dist/proxy/patch/tool-round-limiter.d.ts +1 -1
- package/dist/proxy/patch/tool-round-limiter.js +16 -0
- package/dist/proxy/proxy-core.d.ts +1 -1
- package/dist/proxy/proxy-logging.d.ts +3 -3
- package/dist/proxy/response-transform.js +13 -0
- package/dist/proxy/transform/id-utils.d.ts +1 -0
- package/dist/proxy/transform/id-utils.js +3 -0
- package/dist/proxy/transform/plugin-types.d.ts +5 -5
- package/dist/proxy/transform/request-bridge-responses.d.ts +19 -0
- package/dist/proxy/transform/request-bridge-responses.js +311 -0
- package/dist/proxy/transform/request-transform-responses.d.ts +2 -0
- package/dist/proxy/transform/request-transform-responses.js +350 -0
- package/dist/proxy/transform/response-bridge-responses.d.ts +23 -0
- package/dist/proxy/transform/response-bridge-responses.js +173 -0
- package/dist/proxy/transform/response-transform-responses.d.ts +2 -0
- package/dist/proxy/transform/response-transform-responses.js +137 -0
- package/dist/proxy/transform/stream-ant2resp.d.ts +26 -0
- package/dist/proxy/transform/stream-ant2resp.js +322 -0
- package/dist/proxy/transform/stream-bridge-chat2resp.d.ts +40 -0
- package/dist/proxy/transform/stream-bridge-chat2resp.js +382 -0
- package/dist/proxy/transform/stream-bridge-resp2chat.d.ts +24 -0
- package/dist/proxy/transform/stream-bridge-resp2chat.js +237 -0
- package/dist/proxy/transform/stream-resp2ant.d.ts +21 -0
- package/dist/proxy/transform/stream-resp2ant.js +238 -0
- package/dist/proxy/transform/stream-transform-base.d.ts +1 -0
- package/dist/proxy/transform/stream-transform-base.js +3 -0
- package/dist/proxy/transform/transform-coordinator.d.ts +1 -0
- package/dist/proxy/transform/transform-coordinator.js +127 -8
- package/dist/proxy/transform/types-responses.d.ts +177 -0
- package/dist/proxy/transform/types-responses.js +27 -0
- package/dist/proxy/transform/types.d.ts +3 -1
- package/dist/proxy/transport/transport-fn.d.ts +1 -1
- package/dist/upgrade/checker.js +9 -24
- package/frontend-dist/assets/CardContent-BhMXx-JD.js +1 -0
- package/frontend-dist/assets/CardTitle-DQDjTee3.js +1 -0
- package/frontend-dist/assets/CascadingModelSelect-JBQq3JJt.js +1 -0
- package/frontend-dist/assets/Checkbox-ByxbKP_C.js +1 -0
- package/frontend-dist/assets/CollapsibleContent-GecW2Jk_.js +1 -0
- package/frontend-dist/assets/CollapsibleTrigger-Cib3-OsK.js +1 -0
- package/frontend-dist/assets/Collection-Dbvdpa0m.js +1 -0
- package/frontend-dist/assets/Dashboard-3MJPLflT.js +3 -0
- package/frontend-dist/assets/DialogTitle-Ej_rtfV1.js +1 -0
- package/frontend-dist/assets/{Input-RyuwzbNx.js → Input-tcnrMp1v.js} +1 -1
- package/frontend-dist/assets/Label-BwzPFyL-.js +1 -0
- package/frontend-dist/assets/Login-Cdsw2pWC.js +1 -0
- package/frontend-dist/assets/Logs-5_CWiws5.js +1 -0
- package/frontend-dist/assets/MappingList-D8HRph05.js +1 -0
- package/frontend-dist/assets/ModelCard-CZbQcYNn.js +1 -0
- package/frontend-dist/assets/ModelMappings-CJqgl7O8.js +1 -0
- package/frontend-dist/assets/Monitor-B8v5a8fB.js +1 -0
- package/frontend-dist/assets/PopoverTrigger-C88SpJNZ.js +1 -0
- package/frontend-dist/assets/PopperContent-6BXua_FZ.js +1 -0
- package/frontend-dist/assets/Providers-DH0nvlGn.js +1 -0
- package/frontend-dist/assets/ProxyEnhancement-CAH-44W-.js +5 -0
- package/frontend-dist/assets/QuickSetup-CsDO-ZGP.js +1 -0
- package/frontend-dist/assets/RetryRules-8iT9fLsH.js +1 -0
- package/frontend-dist/assets/RouterKeys-BFoEmWgj.js +1 -0
- package/frontend-dist/assets/RovingFocusItem-DdPUFQHC.js +1 -0
- package/frontend-dist/assets/Schedules-B8Se31u4.js +1 -0
- package/frontend-dist/assets/SelectValue-CT2z_-6j.js +1 -0
- package/frontend-dist/assets/Settings-BHvtsJKD.js +6 -0
- package/frontend-dist/assets/Setup-k-l9KDC0.js +1 -0
- package/frontend-dist/assets/Switch-D1NdA4ax.js +1 -0
- package/frontend-dist/assets/TableHeader-CcMyOsUB.js +1 -0
- package/frontend-dist/assets/Teleport-Bmeh33lB.js +3 -0
- package/frontend-dist/assets/TooltipTrigger-LegC_Uvp.js +1 -0
- package/frontend-dist/assets/UnifiedRequestDialog-BVw6W2pk.js +3 -0
- package/frontend-dist/assets/UnifiedRequestDialog-C4MTxb25.css +1 -0
- package/frontend-dist/assets/VisuallyHidden-ogESfc9X.js +1 -0
- package/frontend-dist/assets/VisuallyHiddenInput-BQemVGau.js +1 -0
- package/frontend-dist/assets/alert-dialog-DzKCAoYJ.js +1 -0
- package/frontend-dist/assets/{badge-CpT5q-jI.js → badge-C-9zPTgw.js} +1 -1
- package/frontend-dist/assets/button-D27ClX8J.js +14 -0
- package/frontend-dist/assets/check-yTAivq1h.js +1 -0
- package/frontend-dist/assets/common-CWCbKHOK.js +1 -0
- package/frontend-dist/assets/common-D4xnnaqi.js +1 -0
- package/frontend-dist/assets/{copy-CIHn6HDL.js → copy-DWG9cQPR.js} +1 -1
- package/frontend-dist/assets/dashboard-B8eI-t8c.js +1 -0
- package/frontend-dist/assets/dashboard-Dbe6A2lu.js +1 -0
- package/frontend-dist/assets/dialog-BnYR6_dh.js +1 -0
- package/frontend-dist/assets/{file-text-LfP0_JRK.js → file-text-D33FJAPX.js} +1 -1
- package/frontend-dist/assets/format-BhxQSgt6.js +1 -0
- package/frontend-dist/assets/i18n-CwUfS0tE.js +1 -0
- package/frontend-dist/assets/index-B348nt-T.css +1 -0
- package/frontend-dist/assets/index-C8DKlnvd.js +1 -0
- package/frontend-dist/assets/lib-D0Ek2pPZ.js +1 -0
- package/frontend-dist/assets/loader-circle-EpKC006I.js +1 -0
- package/frontend-dist/assets/login-BTolYxVI.js +1 -0
- package/frontend-dist/assets/login-w_ICpiU5.js +1 -0
- package/frontend-dist/assets/logs-7dT2uyMa.js +1 -0
- package/frontend-dist/assets/logs-_3w8tDQa.js +1 -0
- package/frontend-dist/assets/mappings-Bbn3r2uJ.js +1 -0
- package/frontend-dist/assets/mappings-CTZ-zb1x.js +1 -0
- package/frontend-dist/assets/monitor-DN5m5n_x.js +1 -0
- package/frontend-dist/assets/monitor-DysWEOtt.js +1 -0
- package/frontend-dist/assets/providers-C1gQGzwa.js +1 -0
- package/frontend-dist/assets/providers-CCfko___.js +1 -0
- package/frontend-dist/assets/proxyEnhancement-BItabyLo.js +1 -0
- package/frontend-dist/assets/proxyEnhancement-DeMb7wIE.js +1 -0
- package/frontend-dist/assets/quickSetup-C75HMC_z.js +1 -0
- package/frontend-dist/assets/quickSetup-DStZWiuf.js +1 -0
- package/frontend-dist/assets/requestDetail-BoaPEQs-.js +1 -0
- package/frontend-dist/assets/requestDetail-CM5kFgy6.js +1 -0
- package/frontend-dist/assets/retryRules-CIF37gOl.js +1 -0
- package/frontend-dist/assets/retryRules-o_D8S5gy.js +1 -0
- package/frontend-dist/assets/routerKeys-BAvjW0V8.js +1 -0
- package/frontend-dist/assets/routerKeys-mQt2YPuE.js +1 -0
- package/frontend-dist/assets/schedules-BCV2rxK-.js +1 -0
- package/frontend-dist/assets/schedules-Qte9b7b_.js +1 -0
- package/frontend-dist/assets/settings-Bgu2lJfy.js +1 -0
- package/frontend-dist/assets/settings-UCmMSq_F.js +1 -0
- package/frontend-dist/assets/setup-B_fAfMoV.js +1 -0
- package/frontend-dist/assets/setup-Chc246Zi.js +1 -0
- package/frontend-dist/assets/sidebar-B7rejnZA.js +1 -0
- package/frontend-dist/assets/sidebar-CBMItLst.js +1 -0
- package/frontend-dist/assets/{sun-n4cC12ho.js → sun-BylRZIWt.js} +1 -1
- package/frontend-dist/assets/{trash-2-oDWBOuqK.js → trash-2-QNFff7V4.js} +1 -1
- package/frontend-dist/assets/{useClipboard-C2i7YvJ-.js → useClipboard-BFt5f-_-.js} +1 -1
- package/frontend-dist/assets/{useFocusGuards-DORIgNd9.js → useFocusGuards-DQBZKWnu.js} +1 -1
- package/frontend-dist/assets/useFormControl-T2RQNBqs.js +1 -0
- package/frontend-dist/assets/useLogRetention-NrrZrpPE.js +1 -0
- package/frontend-dist/assets/useNonce-DR38uny5.js +1 -0
- package/frontend-dist/assets/{useTheme-BFhy-DAX.js → useTheme-CpTI547G.js} +1 -1
- package/frontend-dist/assets/x-DSgLgKC_.js +1 -0
- package/frontend-dist/index.html +25 -24
- package/package.json +1 -1
- package/dist/db/migrations/033_add_pipeline_snapshot.sql +0 -1
- package/frontend-dist/assets/CardContent-F3K9pZNP.js +0 -1
- package/frontend-dist/assets/CardTitle-13anASyk.js +0 -1
- package/frontend-dist/assets/CascadingModelSelect-BmW89GUP.js +0 -1
- package/frontend-dist/assets/Checkbox-C2oSHNgP.js +0 -1
- package/frontend-dist/assets/CollapsibleContent-CdeCo0Ko.js +0 -1
- package/frontend-dist/assets/CollapsibleTrigger-CMd4wTNY.js +0 -1
- package/frontend-dist/assets/Collection-BulopTxo.js +0 -1
- package/frontend-dist/assets/Dashboard-BahJSTKV.js +0 -3
- package/frontend-dist/assets/DialogTitle-CnqbO2hx.js +0 -1
- package/frontend-dist/assets/Label-73u_Os4X.js +0 -1
- package/frontend-dist/assets/Login-CoQSrVLo.js +0 -1
- package/frontend-dist/assets/Logs-C2b6MPXL.js +0 -1
- package/frontend-dist/assets/MappingList-m2ebUmJ9.js +0 -1
- package/frontend-dist/assets/ModelCard-B0pjEq6W.js +0 -1
- package/frontend-dist/assets/ModelMappings-BazKS9T4.js +0 -1
- package/frontend-dist/assets/Monitor-8B_tm1NO.js +0 -1
- package/frontend-dist/assets/PopoverTrigger-DSmA2dE4.js +0 -1
- package/frontend-dist/assets/PopperContent-Bd_mpt_D.js +0 -1
- package/frontend-dist/assets/Providers-TI83sF2T.js +0 -1
- package/frontend-dist/assets/ProxyEnhancement-CWLh-YlM.js +0 -5
- package/frontend-dist/assets/QuickSetup-DUZNdIvp.js +0 -1
- package/frontend-dist/assets/RetryRules-CzhCNQ0R.js +0 -1
- package/frontend-dist/assets/RouterKeys-B_C-Wp_I.js +0 -1
- package/frontend-dist/assets/RovingFocusItem-DwGTruuB.js +0 -1
- package/frontend-dist/assets/Schedules-BMB6RX9e.js +0 -1
- package/frontend-dist/assets/SelectValue-DRc1qira.js +0 -1
- package/frontend-dist/assets/Settings-Ck8CoUJC.js +0 -6
- package/frontend-dist/assets/Setup-dwKkHGrB.js +0 -1
- package/frontend-dist/assets/Switch-BE8DAylK.js +0 -1
- package/frontend-dist/assets/TableHeader-BqYT-eO-.js +0 -1
- package/frontend-dist/assets/Teleport-CLw1Jxrb.js +0 -3
- package/frontend-dist/assets/TooltipTrigger-bqCyq9MU.js +0 -1
- package/frontend-dist/assets/UnifiedRequestDialog-BDNR1wzi.js +0 -3
- package/frontend-dist/assets/UnifiedRequestDialog-DmpjVK9n.css +0 -1
- package/frontend-dist/assets/VisuallyHidden-BpDuyh8-.js +0 -1
- package/frontend-dist/assets/VisuallyHiddenInput-CCL5ykZW.js +0 -1
- package/frontend-dist/assets/alert-dialog-gprnWn1b.js +0 -1
- package/frontend-dist/assets/button-zud8Qspb.js +0 -12
- package/frontend-dist/assets/check-CRv7NpkT.js +0 -1
- package/frontend-dist/assets/dialog-Da8YFS7g.js +0 -1
- package/frontend-dist/assets/format-Dln15Luw.js +0 -1
- package/frontend-dist/assets/index-BfXK7SYr.js +0 -1
- package/frontend-dist/assets/index-CDtb1WVq.css +0 -1
- package/frontend-dist/assets/lib-xfvPneK8.js +0 -1
- package/frontend-dist/assets/loader-circle-D8BaqxEc.js +0 -1
- package/frontend-dist/assets/useFormControl-OyxyVR_M.js +0 -1
- package/frontend-dist/assets/useLogRetention-DE7zYGFK.js +0 -1
- package/frontend-dist/assets/useNonce-D_84NiFG.js +0 -1
- package/frontend-dist/assets/x-BN5AHIVk.js +0 -1
- /package/dist/db/migrations/{034_drop_redundant_log_columns.sql → 035_drop_redundant_log_columns.sql} +0 -0
- /package/frontend-dist/assets/{constants-ncbNnOLM.js → constants-B-VELBjk.js} +0 -0
- /package/frontend-dist/assets/{ohash.D__AXeF1-D5e5Wyzx.js → ohash.D__AXeF1-CTo5WcIm.js} +0 -0
package/README.en.md
ADDED
|
@@ -0,0 +1,319 @@
|
|
|
1
|
+
**[English](README.en.md)** | **[中文](README.md)**
|
|
2
|
+
|
|
3
|
+
# LLM Simple Router
|
|
4
|
+
|
|
5
|
+
An LLM API proxy router that receives requests from clients like Claude Code and Cursor, forwards them to configured backend Providers through model mapping and routing strategies, supporting both streaming (SSE) and non-streaming proxying.
|
|
6
|
+
|
|
7
|
+
**Core problem it solves**: Chinese domestic models have frequent rate limits, switching between multiple providers is cumbersome, and concurrency control is missing.
|
|
8
|
+
|
|
9
|
+
## Who Is This For
|
|
10
|
+
|
|
11
|
+
- Developers using Claude Code with Chinese domestic models (Zhipu, Moonshot, Minimax, etc.)
|
|
12
|
+
- Those who want automatic retries for rate-limit errors, time-based model switching, and concurrency queue management
|
|
13
|
+
- Anyone looking for a turnkey solution without the hassle
|
|
14
|
+
|
|
15
|
+
## Feature Overview
|
|
16
|
+
|
|
17
|
+
| Feature | Description |
|
|
18
|
+
|---------|-------------|
|
|
19
|
+
| Automatic retries | Exponential backoff retries for 429/400/network timeouts, pre-configured for Zhipu models by default |
|
|
20
|
+
| Multi-provider support | Zhipu, Moonshot, Minimax, Volcano Engine, Alibaba Cloud, Tencent Cloud, etc. Base URL is auto-filled when you select a Coding Plan |
|
|
21
|
+
| Time-based model mapping | Automatically switch backend models by time period (e.g., switch to Kimi during peak hours, back to GLM during off-peak) |
|
|
22
|
+
| Concurrency queue | Per-Provider concurrency limits with queueing for excess requests |
|
|
23
|
+
| Failover | Multiple Providers as backups; automatically switches to the next on failure |
|
|
24
|
+
| Real-time monitoring | SSE-based live view of active requests, queue status, and streaming output |
|
|
25
|
+
| Multi-key management | Independent API keys + model whitelists for multi-user/multi-project setups |
|
|
26
|
+
| Request logs | Full four-stage tracing (client request / upstream request / upstream response / client response) |
|
|
27
|
+
| Performance metrics | TTFT, TPS, Token usage, cache hit rate |
|
|
28
|
+
|
|
29
|
+
> **API Compatibility:** Supports Anthropic-compatible API (adapted for Claude Code). OpenAI-compatible API (`/v1/chat/completions`) is not yet fully tested.
|
|
30
|
+
|
|
31
|
+
## Admin Dashboard
|
|
32
|
+
|
|
33
|
+
| Provider Management + Concurrency Control | Real-time Monitoring |
|
|
34
|
+
|---|---|
|
|
35
|
+
|  |  |
|
|
36
|
+
|
|
37
|
+
| Model Mapping | Retry Rules |
|
|
38
|
+
|---|---|
|
|
39
|
+
|  |  |
|
|
40
|
+
|
|
41
|
+
| Dashboard | Request Logs |
|
|
42
|
+
|---|---|
|
|
43
|
+
|  |  |
|
|
44
|
+
|
|
45
|
+
| Proxy Enhancement (Experimental) |
|
|
46
|
+
|----------------------------------|
|
|
47
|
+
|  |
|
|
48
|
+
|
|
49
|
+
## Quick Start
|
|
50
|
+
|
|
51
|
+
### 1. Start the Router
|
|
52
|
+
|
|
53
|
+
```bash
|
|
54
|
+
npx llm-simple-router
|
|
55
|
+
```
|
|
56
|
+
|
|
57
|
+
Visit http://localhost:9981/admin — on first access you'll see the Setup page to set an admin password. Data is stored in `~/.llm-simple-router/`.
|
|
58
|
+
|
|
59
|
+
### 2. Configure a Provider
|
|
60
|
+
|
|
61
|
+
Go to Admin Dashboard > Provider page > Add Provider. Select a Coding Plan and the Base URL will be auto-filled — you only need to provide the API Key.
|
|
62
|
+
|
|
63
|
+
### 3. Configure Model Mapping
|
|
64
|
+
|
|
65
|
+
Go to Admin Dashboard > Model Mapping page.
|
|
66
|
+
|
|
67
|
+
**Core concept:** The client sends a request with model name A. The Router replaces it with model name B (supported by the backend Provider) based on mapping rules, then forwards the request:
|
|
68
|
+
|
|
69
|
+
```
|
|
70
|
+
Claude Code (model A) → Router (A → B) → Provider API (model B)
|
|
71
|
+
```
|
|
72
|
+
|
|
73
|
+
Simply configure "client model = A, backend model = B, select provider" in the mapping table.
|
|
74
|
+
|
|
75
|
+
#### Claude Code Default Model Names
|
|
76
|
+
|
|
77
|
+
When no environment variables are set, Claude Code uses the following default model names: `opus`, `sonnet`, `haiku`. If the backend is Zhipu Coding Plan, the mapping configuration would be:
|
|
78
|
+
|
|
79
|
+
| Client Model | Backend Model | Provider | Time Window |
|
|
80
|
+
|-------------|---------------|----------|-------------|
|
|
81
|
+
| opus | glm-5.1 | Zhipu Coding Plan | All day |
|
|
82
|
+
| sonnet | glm-5.1 | Zhipu Coding Plan | All day |
|
|
83
|
+
| haiku | glm-5-turbo | Zhipu Coding Plan | All day |
|
|
84
|
+
|
|
85
|
+
You can also use time-based mapping to auto-switch during peak hours:
|
|
86
|
+
|
|
87
|
+
| Client Model | Backend Model | Provider | Time Window |
|
|
88
|
+
|-------------|---------------|----------|-------------|
|
|
89
|
+
| sonnet | glm-5.1 | Zhipu Coding Plan | 00:00-14:00 |
|
|
90
|
+
| sonnet | kimi-for-coding | Moonshot | 14:00-18:00 |
|
|
91
|
+
| sonnet | glm-5.1 | Zhipu Coding Plan | 18:00-24:00 |
|
|
92
|
+
|
|
93
|
+
### 4. Configure Claude Code
|
|
94
|
+
|
|
95
|
+
Create a Router API key in the admin dashboard, then choose one of the following methods. **You only need one of the two.**
|
|
96
|
+
|
|
97
|
+
**Method 1: Shell alias (recommended)**
|
|
98
|
+
|
|
99
|
+
Minimal configuration — Claude Code uses default model names (opus / sonnet / haiku), and the Router converts them via the mapping table:
|
|
100
|
+
|
|
101
|
+
```bash
|
|
102
|
+
alias clode='\
|
|
103
|
+
export ANTHROPIC_AUTH_TOKEN="<your-router-key>" && \
|
|
104
|
+
export ANTHROPIC_BASE_URL="http://127.0.0.1:9981" && \
|
|
105
|
+
claude'
|
|
106
|
+
```
|
|
107
|
+
|
|
108
|
+
You can also specify model names directly via environment variables, bypassing Router mapping:
|
|
109
|
+
|
|
110
|
+
```bash
|
|
111
|
+
alias clode='\
|
|
112
|
+
export ANTHROPIC_AUTH_TOKEN="sk-router-xxxxxxxx" && \
|
|
113
|
+
export ANTHROPIC_BASE_URL="http://192.168.1.111:9981" && \
|
|
114
|
+
export ANTHROPIC_MODEL="glm-5" && \
|
|
115
|
+
export ANTHROPIC_DEFAULT_OPUS_MODEL="glm-5.1" && \
|
|
116
|
+
export ANTHROPIC_DEFAULT_SONNET_MODEL="glm-5" && \
|
|
117
|
+
export ANTHROPIC_DEFAULT_HAIKU_MODEL="glm-5-turbo" && \
|
|
118
|
+
export ANTHROPIC_SMALL_FAST_MODEL="glm-5-turbo" && \
|
|
119
|
+
claude'
|
|
120
|
+
```
|
|
121
|
+
|
|
122
|
+
> For debugging, add flags: `claude --dangerously-skip-permissions --verbose --debug`, or set `export DEBUG=claude:*` for detailed logs.
|
|
123
|
+
|
|
124
|
+
**Method 2: ~/.claude/settings.json**
|
|
125
|
+
|
|
126
|
+
Configure in the `env` field of `~/.claude/settings.json` — same effect as exporting environment variables:
|
|
127
|
+
|
|
128
|
+
Minimal configuration:
|
|
129
|
+
|
|
130
|
+
```json
|
|
131
|
+
{
|
|
132
|
+
"env": {
|
|
133
|
+
"ANTHROPIC_AUTH_TOKEN": "<your-router-key>",
|
|
134
|
+
"ANTHROPIC_BASE_URL": "http://127.0.0.1:9981"
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
```
|
|
138
|
+
|
|
139
|
+
Override model names:
|
|
140
|
+
|
|
141
|
+
```json
|
|
142
|
+
{
|
|
143
|
+
"env": {
|
|
144
|
+
"ANTHROPIC_AUTH_TOKEN": "sk-router-xxxxxxxx",
|
|
145
|
+
"ANTHROPIC_BASE_URL": "http://192.168.1.111:9981",
|
|
146
|
+
"ANTHROPIC_MODEL": "glm-5",
|
|
147
|
+
"ANTHROPIC_DEFAULT_OPUS_MODEL": "glm-5.1",
|
|
148
|
+
"ANTHROPIC_DEFAULT_SONNET_MODEL": "glm-5",
|
|
149
|
+
"ANTHROPIC_DEFAULT_HAIKU_MODEL": "glm-5-turbo",
|
|
150
|
+
"ANTHROPIC_SMALL_FAST_MODEL": "glm-5-turbo"
|
|
151
|
+
}
|
|
152
|
+
}
|
|
153
|
+
```
|
|
154
|
+
|
|
155
|
+
> Environment variables in settings.json apply to all projects. To apply only to the current project, place them in `.claude/settings.json` (in the project root).
|
|
156
|
+
|
|
157
|
+
### 5. Usage
|
|
158
|
+
|
|
159
|
+
```bash
|
|
160
|
+
# Method 1 (shell alias)
|
|
161
|
+
clode
|
|
162
|
+
|
|
163
|
+
# Method 2 (settings.json)
|
|
164
|
+
claude
|
|
165
|
+
```
|
|
166
|
+
|
|
167
|
+
## Docker Deployment
|
|
168
|
+
|
|
169
|
+
```bash
|
|
170
|
+
docker compose up -d
|
|
171
|
+
```
|
|
172
|
+
|
|
173
|
+
Environment variables are configured through the Setup page — no `.env` file needed.
|
|
174
|
+
|
|
175
|
+
## Process Management
|
|
176
|
+
|
|
177
|
+
After upgrading via the Web UI, the service needs to restart to take effect. Use one of the following deployment methods to ensure automatic recovery after crashes or upgrade restarts.
|
|
178
|
+
|
|
179
|
+
### PM2 (Recommended)
|
|
180
|
+
|
|
181
|
+
```bash
|
|
182
|
+
# Install PM2
|
|
183
|
+
npm install -g pm2
|
|
184
|
+
|
|
185
|
+
# Install Router globally
|
|
186
|
+
npm install -g llm-simple-router
|
|
187
|
+
|
|
188
|
+
# Start (PM2 auto-restarts crashed processes)
|
|
189
|
+
pm2 start llm-simple-router --name llm-router
|
|
190
|
+
|
|
191
|
+
# View logs
|
|
192
|
+
pm2 logs llm-router
|
|
193
|
+
|
|
194
|
+
# Enable startup on boot
|
|
195
|
+
pm2 startup
|
|
196
|
+
pm2 save
|
|
197
|
+
```
|
|
198
|
+
|
|
199
|
+
Upgrade flow: Web UI one-click upgrade → click restart → PM2 auto-spawns new process (< 1s downtime).
|
|
200
|
+
|
|
201
|
+
### systemd (Linux Servers)
|
|
202
|
+
|
|
203
|
+
Create a service file at `/etc/systemd/system/llm-simple-router.service`:
|
|
204
|
+
|
|
205
|
+
```ini
|
|
206
|
+
[Unit]
|
|
207
|
+
Description=LLM Simple Router
|
|
208
|
+
After=network.target
|
|
209
|
+
|
|
210
|
+
[Service]
|
|
211
|
+
Type=simple
|
|
212
|
+
ExecStart=/usr/local/bin/llm-simple-router
|
|
213
|
+
Restart=always
|
|
214
|
+
RestartSec=3
|
|
215
|
+
Environment=PORT=9981
|
|
216
|
+
Environment=LOG_LEVEL=info
|
|
217
|
+
# Configure other environment variables as needed
|
|
218
|
+
# Environment=DB_PATH=/var/lib/llm-simple-router/router.db
|
|
219
|
+
|
|
220
|
+
[Install]
|
|
221
|
+
WantedBy=multi-user.target
|
|
222
|
+
```
|
|
223
|
+
|
|
224
|
+
> **Note:** The `ExecStart` path depends on how Node.js is installed. Use `which llm-simple-router` to confirm the actual path.
|
|
225
|
+
|
|
226
|
+
```bash
|
|
227
|
+
# Enable and start
|
|
228
|
+
sudo systemctl enable llm-simple-router
|
|
229
|
+
sudo systemctl start llm-simple-router
|
|
230
|
+
|
|
231
|
+
# View status and logs
|
|
232
|
+
sudo systemctl status llm-simple-router
|
|
233
|
+
journalctl -u llm-simple-router -f
|
|
234
|
+
```
|
|
235
|
+
|
|
236
|
+
Upgrade flow: Web UI one-click upgrade → click restart → systemd auto-restarts (< 1s downtime).
|
|
237
|
+
|
|
238
|
+
### npx / Manual Start
|
|
239
|
+
|
|
240
|
+
No extra configuration needed. After upgrading via Web UI and clicking restart, the Router automatically spawns a new process and exits the old one. Brief interruption of about 1-2 seconds.
|
|
241
|
+
|
|
242
|
+
> **Note:** If you directly `Ctrl+C` or close the terminal, the service won't auto-recover. For production, use PM2 or systemd.
|
|
243
|
+
|
|
244
|
+
## How It Works
|
|
245
|
+
|
|
246
|
+
```
|
|
247
|
+
Claude Code → Router (model mapping + auto-retry + concurrency control) → Zhipu GLM / Kimi / Other Providers
|
|
248
|
+
```
|
|
249
|
+
|
|
250
|
+
The Router finds the backend provider via model mapping → forwards the request → auto-retries failed requests → logs and records performance metrics → returns the response.
|
|
251
|
+
|
|
252
|
+
### Architecture Diagram
|
|
253
|
+
|
|
254
|
+
**System Context** ([detailed description](docs/system-context.md)):
|
|
255
|
+
|
|
256
|
+
```mermaid
|
|
257
|
+
graph LR
|
|
258
|
+
Clients["Claude Code / Cursor / Other Clients"]
|
|
259
|
+
Admin["Administrator"]
|
|
260
|
+
Router>"LLM Simple Router"]
|
|
261
|
+
Providers>"Zhipu / Moonshot / OpenAI / Anthropic / ..."]
|
|
262
|
+
|
|
263
|
+
Clients -->|"API Request<br/>Bearer Token"| Router
|
|
264
|
+
Admin -->|"Admin Dashboard<br/>/admin/"| Router
|
|
265
|
+
Router -->|"Forwarded Request<br/>SSE Streaming"| Providers
|
|
266
|
+
```
|
|
267
|
+
|
|
268
|
+
**Request Processing Pipeline** ([detailed description](docs/request-pipeline.md)):
|
|
269
|
+
|
|
270
|
+
```mermaid
|
|
271
|
+
flowchart LR
|
|
272
|
+
A[Client Request] --> B[Authentication]
|
|
273
|
+
B --> C[Model Mapping<br/>+ Routing Strategy]
|
|
274
|
+
C --> D[Concurrency Queue]
|
|
275
|
+
D --> E[Call Upstream<br/>Auto-retry on Failure]
|
|
276
|
+
E --> F[Log Request<br/>+ Metrics]
|
|
277
|
+
F --> G[Return Response]
|
|
278
|
+
|
|
279
|
+
E -.->|Failure| C
|
|
280
|
+
```
|
|
281
|
+
|
|
282
|
+
When the Router receives a request: Authentication → find backend Provider via mapping rules → queue for concurrency control → forward to upstream (auto-retry on failure; under Failover strategy, switches Provider) → log and record metrics → return response.
|
|
283
|
+
|
|
284
|
+
## Environment Variables
|
|
285
|
+
|
|
286
|
+
All secrets are configured through the Setup page. The following are optional configurations:
|
|
287
|
+
|
|
288
|
+
| Variable | Default | Description |
|
|
289
|
+
|----------|---------|-------------|
|
|
290
|
+
| `PORT` | `9981` | Service port |
|
|
291
|
+
| `DB_PATH` | `~/.llm-simple-router/router.db` | SQLite database path |
|
|
292
|
+
| `LOG_LEVEL` | `info` | Log level |
|
|
293
|
+
| `TZ` | `Asia/Shanghai` | Timezone setting |
|
|
294
|
+
| `STREAM_TIMEOUT_MS` | `3000000` | Streaming proxy idle timeout (ms) |
|
|
295
|
+
| `RETRY_MAX_ATTEMPTS` | `3` | Maximum retry attempts |
|
|
296
|
+
| `RETRY_BASE_DELAY_MS` | `1000` | Retry base delay (ms) |
|
|
297
|
+
|
|
298
|
+
## Development
|
|
299
|
+
|
|
300
|
+
```bash
|
|
301
|
+
# Backend (hot reload)
|
|
302
|
+
npm run dev
|
|
303
|
+
|
|
304
|
+
# Frontend (hot reload, proxies API to backend :9980)
|
|
305
|
+
cd frontend && npm run dev
|
|
306
|
+
|
|
307
|
+
# Build
|
|
308
|
+
npm run build:full
|
|
309
|
+
|
|
310
|
+
# Test
|
|
311
|
+
npm test
|
|
312
|
+
|
|
313
|
+
# Lint
|
|
314
|
+
npm run lint
|
|
315
|
+
```
|
|
316
|
+
|
|
317
|
+
## License
|
|
318
|
+
|
|
319
|
+
MIT
|
package/README.md
CHANGED
|
@@ -13,7 +13,7 @@ const API_KEY_PREVIEW_MIN_LENGTH = 8;
|
|
|
13
13
|
const API_KEY_PREVIEW_PREFIX_LEN = 4;
|
|
14
14
|
const QuickSetupProviderSchema = Type.Object({
|
|
15
15
|
name: Type.String({ minLength: 1 }),
|
|
16
|
-
api_type: Type.Union([Type.Literal("openai"), Type.Literal("anthropic")]),
|
|
16
|
+
api_type: Type.Union([Type.Literal("openai"), Type.Literal("openai-responses"), Type.Literal("anthropic")]),
|
|
17
17
|
base_url: Type.String({ minLength: 1 }),
|
|
18
18
|
api_key: Type.String({ minLength: 1 }),
|
|
19
19
|
models: Type.Array(Type.Object({
|
package/dist/admin/upgrade.js
CHANGED
|
@@ -120,9 +120,10 @@ export const adminUpgradeRoutes = (app, options, done) => {
|
|
|
120
120
|
const configDir = path.resolve(process.cwd(), 'config');
|
|
121
121
|
try {
|
|
122
122
|
fs.mkdirSync(configDir, { recursive: true });
|
|
123
|
-
const [providersResult, rulesResult] = await Promise.allSettled([
|
|
123
|
+
const [providersResult, rulesResult, versionResult] = await Promise.allSettled([
|
|
124
124
|
fetchJson(`${base}/recommended-providers.json`),
|
|
125
125
|
fetchJson(`${base}/recommended-retry-rules.json`),
|
|
126
|
+
fetchJson(`${base}/version.json`),
|
|
126
127
|
]);
|
|
127
128
|
if (providersResult.status === 'fulfilled') {
|
|
128
129
|
fs.writeFileSync(path.join(configDir, 'recommended-providers.json'), JSON.stringify(providersResult.value, null, JSON_INDENT));
|
|
@@ -130,9 +131,15 @@ export const adminUpgradeRoutes = (app, options, done) => {
|
|
|
130
131
|
if (rulesResult.status === 'fulfilled') {
|
|
131
132
|
fs.writeFileSync(path.join(configDir, 'recommended-retry-rules.json'), JSON.stringify(rulesResult.value, null, JSON_INDENT));
|
|
132
133
|
}
|
|
134
|
+
if (versionResult.status === 'fulfilled') {
|
|
135
|
+
fs.writeFileSync(path.join(configDir, 'version.json'), JSON.stringify(versionResult.value, null, JSON_INDENT));
|
|
136
|
+
}
|
|
133
137
|
if (providersResult.status === 'rejected' && rulesResult.status === 'rejected') {
|
|
134
138
|
throw new Error('同步失败: 无法获取 providers 和 retry-rules 配置');
|
|
135
139
|
}
|
|
140
|
+
if (versionResult.status === 'rejected') {
|
|
141
|
+
process.stderr.write('[upgrade] warning: version.json sync failed, providers/rules synced without version\n');
|
|
142
|
+
}
|
|
136
143
|
reloadConfig();
|
|
137
144
|
if (checker)
|
|
138
145
|
await checker.check(getConfigBaseUrl(source));
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
export interface ProviderPreset {
|
|
2
2
|
plan: string;
|
|
3
3
|
presetName: string;
|
|
4
|
-
apiType: 'openai' | 'anthropic';
|
|
4
|
+
apiType: 'openai' | 'openai-responses' | 'anthropic';
|
|
5
5
|
baseUrl: string;
|
|
6
6
|
models: string[];
|
|
7
7
|
}
|
|
@@ -19,7 +19,13 @@ export interface RecommendedRetryRule {
|
|
|
19
19
|
max_delay_ms: number;
|
|
20
20
|
providers?: string[];
|
|
21
21
|
}
|
|
22
|
+
export interface ConfigVersions {
|
|
23
|
+
providers: number;
|
|
24
|
+
retryRules: number;
|
|
25
|
+
}
|
|
22
26
|
export declare function loadRecommendedConfig(dir?: string): void;
|
|
23
27
|
export declare function getRecommendedProviders(): ProviderGroup[];
|
|
24
28
|
export declare function getRecommendedRetryRules(): RecommendedRetryRule[];
|
|
25
29
|
export declare function reloadConfig(): void;
|
|
30
|
+
/** 读取推荐配置的版本号(来自独立 version.json,历史版本代码不会读取此文件) */
|
|
31
|
+
export declare function getConfigVersions(): ConfigVersions;
|
|
@@ -13,6 +13,18 @@ export function getRecommendedRetryRules() {
|
|
|
13
13
|
// No-op: kept for backward compat (reload endpoint, upgrade flow)
|
|
14
14
|
// Config is now always read from disk, no caching.
|
|
15
15
|
export function reloadConfig() { }
|
|
16
|
+
/** 读取推荐配置的版本号(来自独立 version.json,历史版本代码不会读取此文件) */
|
|
17
|
+
export function getConfigVersions() {
|
|
18
|
+
const filePath = path.join(configDir, 'version.json');
|
|
19
|
+
try {
|
|
20
|
+
if (!fs.existsSync(filePath))
|
|
21
|
+
return { providers: 0, retryRules: 0 };
|
|
22
|
+
return JSON.parse(fs.readFileSync(filePath, 'utf-8'));
|
|
23
|
+
}
|
|
24
|
+
catch {
|
|
25
|
+
return { providers: 0, retryRules: 0 };
|
|
26
|
+
}
|
|
27
|
+
}
|
|
16
28
|
function loadJson(filename) {
|
|
17
29
|
const filePath = path.join(configDir, filename);
|
|
18
30
|
try {
|
package/dist/core/constants.js
CHANGED
|
@@ -14,6 +14,8 @@ export const PROXY_API_TYPES = {
|
|
|
14
14
|
"/v1/chat/completions": "openai",
|
|
15
15
|
"/v1/models": "openai",
|
|
16
16
|
"/v1/messages": "anthropic",
|
|
17
|
+
"/v1/responses": "openai-responses",
|
|
18
|
+
"/responses": "openai-responses",
|
|
17
19
|
};
|
|
18
20
|
export function getProxyApiType(url) {
|
|
19
21
|
const path = url.split("?")[0];
|
package/dist/db/index.js
CHANGED
|
@@ -14,6 +14,11 @@ const MIGRATION_RENAMES = {
|
|
|
14
14
|
"028_convert_old_rule_format.sql": "029_convert_old_rule_format.sql",
|
|
15
15
|
"029_add_input_tokens_estimated.sql": "030_add_input_tokens_estimated.sql",
|
|
16
16
|
"030_add_tps_breakdown.sql": "031_add_tps_breakdown.sql",
|
|
17
|
+
// 消除双 033/034,重新编号 035→038
|
|
18
|
+
"033_add_pipeline_snapshot.sql": "033_add_adaptive_concurrency.sql",
|
|
19
|
+
"034_drop_redundant_log_columns.sql": "035_drop_redundant_log_columns.sql",
|
|
20
|
+
"035_add_openai_responses_api_type.sql": "036_add_openai_responses_api_type.sql",
|
|
21
|
+
"036_fix_035_data_corruption.sql": "037_fix_035_data_corruption.sql",
|
|
17
22
|
};
|
|
18
23
|
export function initDatabase(dbPath) {
|
|
19
24
|
if (dbPath !== ":memory:") {
|
|
@@ -1,3 +1,6 @@
|
|
|
1
1
|
-- 033_add_adaptive_concurrency.sql
|
|
2
2
|
ALTER TABLE providers ADD COLUMN adaptive_enabled INTEGER NOT NULL DEFAULT 0;
|
|
3
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,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;
|
package/dist/db/providers.d.ts
CHANGED
|
@@ -2,7 +2,7 @@ import Database from "better-sqlite3";
|
|
|
2
2
|
export interface Provider {
|
|
3
3
|
id: string;
|
|
4
4
|
name: string;
|
|
5
|
-
api_type: "openai" | "anthropic";
|
|
5
|
+
api_type: "openai" | "openai-responses" | "anthropic";
|
|
6
6
|
base_url: string;
|
|
7
7
|
api_key: string;
|
|
8
8
|
api_key_preview?: string;
|
|
@@ -20,12 +20,12 @@ export declare const PROVIDER_CONCURRENCY_DEFAULTS: {
|
|
|
20
20
|
readonly queue_timeout_ms: 0;
|
|
21
21
|
readonly max_queue_size: 100;
|
|
22
22
|
};
|
|
23
|
-
export declare function getActiveProviders(db: Database.Database, apiType: "openai" | "anthropic"): Provider[];
|
|
23
|
+
export declare function getActiveProviders(db: Database.Database, apiType: "openai" | "openai-responses" | "anthropic"): Provider[];
|
|
24
24
|
export declare function getAllProviders(db: Database.Database): Provider[];
|
|
25
25
|
export declare function getProviderById(db: Database.Database, id: string): Provider | undefined;
|
|
26
26
|
export declare function createProvider(db: Database.Database, provider: {
|
|
27
27
|
name: string;
|
|
28
|
-
api_type: "openai" | "anthropic";
|
|
28
|
+
api_type: "openai" | "openai-responses" | "anthropic";
|
|
29
29
|
base_url: string;
|
|
30
30
|
api_key: string;
|
|
31
31
|
api_key_preview?: string;
|
package/dist/index.js
CHANGED
|
@@ -17,6 +17,7 @@ import { loadRecommendedConfig } from "./config/recommended.js";
|
|
|
17
17
|
import { authMiddleware } from "./middleware/auth.js";
|
|
18
18
|
import { openaiProxy } from "./proxy/handler/openai.js";
|
|
19
19
|
import { anthropicProxy } from "./proxy/handler/anthropic.js";
|
|
20
|
+
import { responsesProxy } from "./proxy/handler/responses.js";
|
|
20
21
|
import { adminRoutes } from "./admin/routes.js";
|
|
21
22
|
import { RetryRuleMatcher } from "./proxy/orchestration/retry-rules.js";
|
|
22
23
|
import { PluginRegistry } from "./proxy/transform/plugin-registry.js";
|
|
@@ -230,6 +231,7 @@ export async function buildApp(options) {
|
|
|
230
231
|
app.register(authMiddleware, { db });
|
|
231
232
|
app.register(openaiProxy, { db, container });
|
|
232
233
|
app.register(anthropicProxy, { db, container });
|
|
234
|
+
app.register(responsesProxy, { db, container });
|
|
233
235
|
// StateRegistry — Admin 层通过此接口触发 proxy 层状态刷新,消除 admin→proxy 依赖
|
|
234
236
|
const stateRegistry = {
|
|
235
237
|
refreshRetryRules: () => matcher.load(db),
|
|
@@ -344,13 +346,15 @@ export async function main() {
|
|
|
344
346
|
}
|
|
345
347
|
/* eslint-enable taste/no-silent-catch */
|
|
346
348
|
});
|
|
347
|
-
// 优雅关闭:SIGTERM
|
|
349
|
+
// 优雅关闭:SIGTERM 和 SIGINT(Ctrl+C)
|
|
350
|
+
// 首次 = 优雅关闭,再次 = 强制退出
|
|
348
351
|
let isShuttingDown = false;
|
|
349
352
|
const GRACEFUL_SHUTDOWN_TIMEOUT_MS = 10_000;
|
|
350
353
|
const shutdown = async (signal) => {
|
|
351
|
-
//
|
|
354
|
+
// 第二次收到信号 = 强制退出(Ctrl+C 卡住时用户可再按一次)
|
|
352
355
|
if (isShuttingDown) {
|
|
353
|
-
app.log.
|
|
356
|
+
app.log.warn(`Received ${signal} again, forcing exit`);
|
|
357
|
+
process.exit(1);
|
|
354
358
|
return;
|
|
355
359
|
}
|
|
356
360
|
isShuttingDown = true;
|
|
@@ -20,10 +20,11 @@ export declare class MetricsExtractor {
|
|
|
20
20
|
private textStreamStartTime;
|
|
21
21
|
private toolUseContentBuffer;
|
|
22
22
|
private toolUseStreamStartTime;
|
|
23
|
-
constructor(apiType: "openai" | "anthropic", requestStartTime: number);
|
|
23
|
+
constructor(apiType: "openai" | "openai-responses" | "anthropic", requestStartTime: number);
|
|
24
24
|
processEvent(event: SSEEvent): void;
|
|
25
25
|
getMetrics(): MetricsResult;
|
|
26
|
-
static fromNonStreamResponse(apiType: "openai" | "anthropic", responseBody: string): MetricsResult | null;
|
|
26
|
+
static fromNonStreamResponse(apiType: "openai" | "openai-responses" | "anthropic", responseBody: string): MetricsResult | null;
|
|
27
|
+
private processResponsesEvent;
|
|
27
28
|
private processAnthropicEvent;
|
|
28
29
|
private processOpenAIEvent;
|
|
29
30
|
}
|