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.
Files changed (212) hide show
  1. package/README.en.md +319 -0
  2. package/README.md +2 -0
  3. package/config/version.json +4 -0
  4. package/dist/admin/quick-setup.js +1 -1
  5. package/dist/admin/upgrade.js +8 -1
  6. package/dist/config/recommended.d.ts +7 -1
  7. package/dist/config/recommended.js +12 -0
  8. package/dist/core/constants.js +2 -0
  9. package/dist/db/index.js +5 -0
  10. package/dist/db/migrations/033_add_adaptive_concurrency.sql +3 -0
  11. package/dist/db/migrations/036_add_openai_responses_api_type.sql +68 -0
  12. package/dist/db/migrations/037_fix_035_data_corruption.sql +54 -0
  13. package/dist/db/providers.d.ts +3 -3
  14. package/dist/index.js +7 -3
  15. package/dist/metrics/metrics-extractor.d.ts +3 -2
  16. package/dist/metrics/metrics-extractor.js +45 -0
  17. package/dist/metrics/sse-metrics-transform.d.ts +1 -1
  18. package/dist/metrics/sse-metrics-transform.js +10 -0
  19. package/dist/monitor/request-tracker.d.ts +1 -1
  20. package/dist/monitor/stream-content-accumulator.d.ts +1 -1
  21. package/dist/monitor/stream-extractor.d.ts +1 -1
  22. package/dist/monitor/stream-extractor.js +21 -0
  23. package/dist/monitor/types.d.ts +1 -1
  24. package/dist/proxy/handler/proxy-handler-utils.d.ts +1 -1
  25. package/dist/proxy/handler/proxy-handler.d.ts +1 -1
  26. package/dist/proxy/handler/responses.d.ts +7 -0
  27. package/dist/proxy/handler/responses.js +48 -0
  28. package/dist/proxy/loop-prevention/tool-loop-guard.d.ts +1 -1
  29. package/dist/proxy/loop-prevention/tool-loop-guard.js +10 -0
  30. package/dist/proxy/orchestration/orchestrator.d.ts +1 -1
  31. package/dist/proxy/orchestration/semaphore.js +6 -0
  32. package/dist/proxy/patch/deepseek/index.d.ts +1 -1
  33. package/dist/proxy/patch/deepseek/patch-thinking-param.d.ts +1 -1
  34. package/dist/proxy/patch/tool-round-limiter.d.ts +1 -1
  35. package/dist/proxy/patch/tool-round-limiter.js +16 -0
  36. package/dist/proxy/proxy-core.d.ts +1 -1
  37. package/dist/proxy/proxy-logging.d.ts +3 -3
  38. package/dist/proxy/response-transform.js +13 -0
  39. package/dist/proxy/transform/id-utils.d.ts +1 -0
  40. package/dist/proxy/transform/id-utils.js +3 -0
  41. package/dist/proxy/transform/plugin-types.d.ts +5 -5
  42. package/dist/proxy/transform/request-bridge-responses.d.ts +19 -0
  43. package/dist/proxy/transform/request-bridge-responses.js +311 -0
  44. package/dist/proxy/transform/request-transform-responses.d.ts +2 -0
  45. package/dist/proxy/transform/request-transform-responses.js +350 -0
  46. package/dist/proxy/transform/response-bridge-responses.d.ts +23 -0
  47. package/dist/proxy/transform/response-bridge-responses.js +173 -0
  48. package/dist/proxy/transform/response-transform-responses.d.ts +2 -0
  49. package/dist/proxy/transform/response-transform-responses.js +137 -0
  50. package/dist/proxy/transform/stream-ant2resp.d.ts +26 -0
  51. package/dist/proxy/transform/stream-ant2resp.js +322 -0
  52. package/dist/proxy/transform/stream-bridge-chat2resp.d.ts +40 -0
  53. package/dist/proxy/transform/stream-bridge-chat2resp.js +382 -0
  54. package/dist/proxy/transform/stream-bridge-resp2chat.d.ts +24 -0
  55. package/dist/proxy/transform/stream-bridge-resp2chat.js +237 -0
  56. package/dist/proxy/transform/stream-resp2ant.d.ts +21 -0
  57. package/dist/proxy/transform/stream-resp2ant.js +238 -0
  58. package/dist/proxy/transform/stream-transform-base.d.ts +1 -0
  59. package/dist/proxy/transform/stream-transform-base.js +3 -0
  60. package/dist/proxy/transform/transform-coordinator.d.ts +1 -0
  61. package/dist/proxy/transform/transform-coordinator.js +127 -8
  62. package/dist/proxy/transform/types-responses.d.ts +177 -0
  63. package/dist/proxy/transform/types-responses.js +27 -0
  64. package/dist/proxy/transform/types.d.ts +3 -1
  65. package/dist/proxy/transport/transport-fn.d.ts +1 -1
  66. package/dist/upgrade/checker.js +9 -24
  67. package/frontend-dist/assets/CardContent-BhMXx-JD.js +1 -0
  68. package/frontend-dist/assets/CardTitle-DQDjTee3.js +1 -0
  69. package/frontend-dist/assets/CascadingModelSelect-JBQq3JJt.js +1 -0
  70. package/frontend-dist/assets/Checkbox-ByxbKP_C.js +1 -0
  71. package/frontend-dist/assets/CollapsibleContent-GecW2Jk_.js +1 -0
  72. package/frontend-dist/assets/CollapsibleTrigger-Cib3-OsK.js +1 -0
  73. package/frontend-dist/assets/Collection-Dbvdpa0m.js +1 -0
  74. package/frontend-dist/assets/Dashboard-3MJPLflT.js +3 -0
  75. package/frontend-dist/assets/DialogTitle-Ej_rtfV1.js +1 -0
  76. package/frontend-dist/assets/{Input-RyuwzbNx.js → Input-tcnrMp1v.js} +1 -1
  77. package/frontend-dist/assets/Label-BwzPFyL-.js +1 -0
  78. package/frontend-dist/assets/Login-Cdsw2pWC.js +1 -0
  79. package/frontend-dist/assets/Logs-5_CWiws5.js +1 -0
  80. package/frontend-dist/assets/MappingList-D8HRph05.js +1 -0
  81. package/frontend-dist/assets/ModelCard-CZbQcYNn.js +1 -0
  82. package/frontend-dist/assets/ModelMappings-CJqgl7O8.js +1 -0
  83. package/frontend-dist/assets/Monitor-B8v5a8fB.js +1 -0
  84. package/frontend-dist/assets/PopoverTrigger-C88SpJNZ.js +1 -0
  85. package/frontend-dist/assets/PopperContent-6BXua_FZ.js +1 -0
  86. package/frontend-dist/assets/Providers-DH0nvlGn.js +1 -0
  87. package/frontend-dist/assets/ProxyEnhancement-CAH-44W-.js +5 -0
  88. package/frontend-dist/assets/QuickSetup-CsDO-ZGP.js +1 -0
  89. package/frontend-dist/assets/RetryRules-8iT9fLsH.js +1 -0
  90. package/frontend-dist/assets/RouterKeys-BFoEmWgj.js +1 -0
  91. package/frontend-dist/assets/RovingFocusItem-DdPUFQHC.js +1 -0
  92. package/frontend-dist/assets/Schedules-B8Se31u4.js +1 -0
  93. package/frontend-dist/assets/SelectValue-CT2z_-6j.js +1 -0
  94. package/frontend-dist/assets/Settings-BHvtsJKD.js +6 -0
  95. package/frontend-dist/assets/Setup-k-l9KDC0.js +1 -0
  96. package/frontend-dist/assets/Switch-D1NdA4ax.js +1 -0
  97. package/frontend-dist/assets/TableHeader-CcMyOsUB.js +1 -0
  98. package/frontend-dist/assets/Teleport-Bmeh33lB.js +3 -0
  99. package/frontend-dist/assets/TooltipTrigger-LegC_Uvp.js +1 -0
  100. package/frontend-dist/assets/UnifiedRequestDialog-BVw6W2pk.js +3 -0
  101. package/frontend-dist/assets/UnifiedRequestDialog-C4MTxb25.css +1 -0
  102. package/frontend-dist/assets/VisuallyHidden-ogESfc9X.js +1 -0
  103. package/frontend-dist/assets/VisuallyHiddenInput-BQemVGau.js +1 -0
  104. package/frontend-dist/assets/alert-dialog-DzKCAoYJ.js +1 -0
  105. package/frontend-dist/assets/{badge-CpT5q-jI.js → badge-C-9zPTgw.js} +1 -1
  106. package/frontend-dist/assets/button-D27ClX8J.js +14 -0
  107. package/frontend-dist/assets/check-yTAivq1h.js +1 -0
  108. package/frontend-dist/assets/common-CWCbKHOK.js +1 -0
  109. package/frontend-dist/assets/common-D4xnnaqi.js +1 -0
  110. package/frontend-dist/assets/{copy-CIHn6HDL.js → copy-DWG9cQPR.js} +1 -1
  111. package/frontend-dist/assets/dashboard-B8eI-t8c.js +1 -0
  112. package/frontend-dist/assets/dashboard-Dbe6A2lu.js +1 -0
  113. package/frontend-dist/assets/dialog-BnYR6_dh.js +1 -0
  114. package/frontend-dist/assets/{file-text-LfP0_JRK.js → file-text-D33FJAPX.js} +1 -1
  115. package/frontend-dist/assets/format-BhxQSgt6.js +1 -0
  116. package/frontend-dist/assets/i18n-CwUfS0tE.js +1 -0
  117. package/frontend-dist/assets/index-B348nt-T.css +1 -0
  118. package/frontend-dist/assets/index-C8DKlnvd.js +1 -0
  119. package/frontend-dist/assets/lib-D0Ek2pPZ.js +1 -0
  120. package/frontend-dist/assets/loader-circle-EpKC006I.js +1 -0
  121. package/frontend-dist/assets/login-BTolYxVI.js +1 -0
  122. package/frontend-dist/assets/login-w_ICpiU5.js +1 -0
  123. package/frontend-dist/assets/logs-7dT2uyMa.js +1 -0
  124. package/frontend-dist/assets/logs-_3w8tDQa.js +1 -0
  125. package/frontend-dist/assets/mappings-Bbn3r2uJ.js +1 -0
  126. package/frontend-dist/assets/mappings-CTZ-zb1x.js +1 -0
  127. package/frontend-dist/assets/monitor-DN5m5n_x.js +1 -0
  128. package/frontend-dist/assets/monitor-DysWEOtt.js +1 -0
  129. package/frontend-dist/assets/providers-C1gQGzwa.js +1 -0
  130. package/frontend-dist/assets/providers-CCfko___.js +1 -0
  131. package/frontend-dist/assets/proxyEnhancement-BItabyLo.js +1 -0
  132. package/frontend-dist/assets/proxyEnhancement-DeMb7wIE.js +1 -0
  133. package/frontend-dist/assets/quickSetup-C75HMC_z.js +1 -0
  134. package/frontend-dist/assets/quickSetup-DStZWiuf.js +1 -0
  135. package/frontend-dist/assets/requestDetail-BoaPEQs-.js +1 -0
  136. package/frontend-dist/assets/requestDetail-CM5kFgy6.js +1 -0
  137. package/frontend-dist/assets/retryRules-CIF37gOl.js +1 -0
  138. package/frontend-dist/assets/retryRules-o_D8S5gy.js +1 -0
  139. package/frontend-dist/assets/routerKeys-BAvjW0V8.js +1 -0
  140. package/frontend-dist/assets/routerKeys-mQt2YPuE.js +1 -0
  141. package/frontend-dist/assets/schedules-BCV2rxK-.js +1 -0
  142. package/frontend-dist/assets/schedules-Qte9b7b_.js +1 -0
  143. package/frontend-dist/assets/settings-Bgu2lJfy.js +1 -0
  144. package/frontend-dist/assets/settings-UCmMSq_F.js +1 -0
  145. package/frontend-dist/assets/setup-B_fAfMoV.js +1 -0
  146. package/frontend-dist/assets/setup-Chc246Zi.js +1 -0
  147. package/frontend-dist/assets/sidebar-B7rejnZA.js +1 -0
  148. package/frontend-dist/assets/sidebar-CBMItLst.js +1 -0
  149. package/frontend-dist/assets/{sun-n4cC12ho.js → sun-BylRZIWt.js} +1 -1
  150. package/frontend-dist/assets/{trash-2-oDWBOuqK.js → trash-2-QNFff7V4.js} +1 -1
  151. package/frontend-dist/assets/{useClipboard-C2i7YvJ-.js → useClipboard-BFt5f-_-.js} +1 -1
  152. package/frontend-dist/assets/{useFocusGuards-DORIgNd9.js → useFocusGuards-DQBZKWnu.js} +1 -1
  153. package/frontend-dist/assets/useFormControl-T2RQNBqs.js +1 -0
  154. package/frontend-dist/assets/useLogRetention-NrrZrpPE.js +1 -0
  155. package/frontend-dist/assets/useNonce-DR38uny5.js +1 -0
  156. package/frontend-dist/assets/{useTheme-BFhy-DAX.js → useTheme-CpTI547G.js} +1 -1
  157. package/frontend-dist/assets/x-DSgLgKC_.js +1 -0
  158. package/frontend-dist/index.html +25 -24
  159. package/package.json +1 -1
  160. package/dist/db/migrations/033_add_pipeline_snapshot.sql +0 -1
  161. package/frontend-dist/assets/CardContent-F3K9pZNP.js +0 -1
  162. package/frontend-dist/assets/CardTitle-13anASyk.js +0 -1
  163. package/frontend-dist/assets/CascadingModelSelect-BmW89GUP.js +0 -1
  164. package/frontend-dist/assets/Checkbox-C2oSHNgP.js +0 -1
  165. package/frontend-dist/assets/CollapsibleContent-CdeCo0Ko.js +0 -1
  166. package/frontend-dist/assets/CollapsibleTrigger-CMd4wTNY.js +0 -1
  167. package/frontend-dist/assets/Collection-BulopTxo.js +0 -1
  168. package/frontend-dist/assets/Dashboard-BahJSTKV.js +0 -3
  169. package/frontend-dist/assets/DialogTitle-CnqbO2hx.js +0 -1
  170. package/frontend-dist/assets/Label-73u_Os4X.js +0 -1
  171. package/frontend-dist/assets/Login-CoQSrVLo.js +0 -1
  172. package/frontend-dist/assets/Logs-C2b6MPXL.js +0 -1
  173. package/frontend-dist/assets/MappingList-m2ebUmJ9.js +0 -1
  174. package/frontend-dist/assets/ModelCard-B0pjEq6W.js +0 -1
  175. package/frontend-dist/assets/ModelMappings-BazKS9T4.js +0 -1
  176. package/frontend-dist/assets/Monitor-8B_tm1NO.js +0 -1
  177. package/frontend-dist/assets/PopoverTrigger-DSmA2dE4.js +0 -1
  178. package/frontend-dist/assets/PopperContent-Bd_mpt_D.js +0 -1
  179. package/frontend-dist/assets/Providers-TI83sF2T.js +0 -1
  180. package/frontend-dist/assets/ProxyEnhancement-CWLh-YlM.js +0 -5
  181. package/frontend-dist/assets/QuickSetup-DUZNdIvp.js +0 -1
  182. package/frontend-dist/assets/RetryRules-CzhCNQ0R.js +0 -1
  183. package/frontend-dist/assets/RouterKeys-B_C-Wp_I.js +0 -1
  184. package/frontend-dist/assets/RovingFocusItem-DwGTruuB.js +0 -1
  185. package/frontend-dist/assets/Schedules-BMB6RX9e.js +0 -1
  186. package/frontend-dist/assets/SelectValue-DRc1qira.js +0 -1
  187. package/frontend-dist/assets/Settings-Ck8CoUJC.js +0 -6
  188. package/frontend-dist/assets/Setup-dwKkHGrB.js +0 -1
  189. package/frontend-dist/assets/Switch-BE8DAylK.js +0 -1
  190. package/frontend-dist/assets/TableHeader-BqYT-eO-.js +0 -1
  191. package/frontend-dist/assets/Teleport-CLw1Jxrb.js +0 -3
  192. package/frontend-dist/assets/TooltipTrigger-bqCyq9MU.js +0 -1
  193. package/frontend-dist/assets/UnifiedRequestDialog-BDNR1wzi.js +0 -3
  194. package/frontend-dist/assets/UnifiedRequestDialog-DmpjVK9n.css +0 -1
  195. package/frontend-dist/assets/VisuallyHidden-BpDuyh8-.js +0 -1
  196. package/frontend-dist/assets/VisuallyHiddenInput-CCL5ykZW.js +0 -1
  197. package/frontend-dist/assets/alert-dialog-gprnWn1b.js +0 -1
  198. package/frontend-dist/assets/button-zud8Qspb.js +0 -12
  199. package/frontend-dist/assets/check-CRv7NpkT.js +0 -1
  200. package/frontend-dist/assets/dialog-Da8YFS7g.js +0 -1
  201. package/frontend-dist/assets/format-Dln15Luw.js +0 -1
  202. package/frontend-dist/assets/index-BfXK7SYr.js +0 -1
  203. package/frontend-dist/assets/index-CDtb1WVq.css +0 -1
  204. package/frontend-dist/assets/lib-xfvPneK8.js +0 -1
  205. package/frontend-dist/assets/loader-circle-D8BaqxEc.js +0 -1
  206. package/frontend-dist/assets/useFormControl-OyxyVR_M.js +0 -1
  207. package/frontend-dist/assets/useLogRetention-DE7zYGFK.js +0 -1
  208. package/frontend-dist/assets/useNonce-D_84NiFG.js +0 -1
  209. package/frontend-dist/assets/x-BN5AHIVk.js +0 -1
  210. /package/dist/db/migrations/{034_drop_redundant_log_columns.sql → 035_drop_redundant_log_columns.sql} +0 -0
  211. /package/frontend-dist/assets/{constants-ncbNnOLM.js → constants-B-VELBjk.js} +0 -0
  212. /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
+ | ![Provider](docs/screenshot/provider_concurrency.png) | ![Monitor](docs/screenshot/monitor.png) |
36
+
37
+ | Model Mapping | Retry Rules |
38
+ |---|---|
39
+ | ![Mapping](docs/screenshot/model_mapping.png) | ![Retry](docs/screenshot/retry.png) |
40
+
41
+ | Dashboard | Request Logs |
42
+ |---|---|
43
+ | ![Dashboard](docs/screenshot/dashboard.png) | ![Logs](docs/screenshot/log.png) |
44
+
45
+ | Proxy Enhancement (Experimental) |
46
+ |----------------------------------|
47
+ | ![Proxy Enhancement](docs/screenshot/proxy_enhance.png) |
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
@@ -1,3 +1,5 @@
1
+ **[English](README.en.md)** | **[中文](README.md)**
2
+
1
3
  # LLM Simple Router
2
4
 
3
5
  LLM API 代理路由器。接收 Claude Code / Cursor 等客户端请求,通过模型映射和路由策略转发到配置的后端 Provider,支持流式(SSE)和非流式代理。
@@ -0,0 +1,4 @@
1
+ {
2
+ "providers": 1,
3
+ "retryRules": 1
4
+ }
@@ -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({
@@ -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 {
@@ -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;
@@ -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(systemd/docker stop)和 SIGINT(Ctrl+C)
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
- // 防止重复触发:多次 Ctrl+C 只执行一次关闭
354
+ // 第二次收到信号 = 强制退出(Ctrl+C 卡住时用户可再按一次)
352
355
  if (isShuttingDown) {
353
- app.log.info(`Received ${signal} again, already shutting down...`);
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
  }