llm-simple-router 0.8.2 → 0.9.4

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 (222) hide show
  1. package/README.en.md +319 -0
  2. package/README.md +2 -0
  3. package/config/recommended-providers.json +33 -9
  4. package/config/recommended-retry-rules.json +9 -8
  5. package/dist/admin/providers.js +11 -9
  6. package/dist/admin/quick-setup.d.ts +13 -0
  7. package/dist/admin/quick-setup.js +169 -0
  8. package/dist/admin/recommended.js +5 -1
  9. package/dist/admin/routes.js +2 -0
  10. package/dist/config/model-context.d.ts +8 -2
  11. package/dist/config/model-context.js +17 -5
  12. package/dist/config/recommended.d.ts +2 -1
  13. package/dist/config/recommended.js +5 -9
  14. package/dist/core/constants.js +2 -0
  15. package/dist/db/index.js +5 -0
  16. package/dist/db/migrations/033_add_adaptive_concurrency.sql +3 -0
  17. package/dist/db/migrations/036_add_openai_responses_api_type.sql +68 -0
  18. package/dist/db/migrations/037_fix_035_data_corruption.sql +54 -0
  19. package/dist/db/providers.d.ts +3 -3
  20. package/dist/index.js +7 -3
  21. package/dist/metrics/metrics-extractor.d.ts +3 -2
  22. package/dist/metrics/metrics-extractor.js +45 -0
  23. package/dist/metrics/sse-metrics-transform.d.ts +1 -1
  24. package/dist/metrics/sse-metrics-transform.js +10 -0
  25. package/dist/monitor/request-tracker.d.ts +1 -1
  26. package/dist/monitor/stream-content-accumulator.d.ts +1 -1
  27. package/dist/monitor/stream-extractor.d.ts +1 -1
  28. package/dist/monitor/stream-extractor.js +21 -0
  29. package/dist/monitor/types.d.ts +1 -1
  30. package/dist/proxy/handler/proxy-handler-utils.d.ts +1 -1
  31. package/dist/proxy/handler/proxy-handler.d.ts +1 -1
  32. package/dist/proxy/handler/proxy-handler.js +8 -2
  33. package/dist/proxy/handler/responses.d.ts +7 -0
  34. package/dist/proxy/handler/responses.js +48 -0
  35. package/dist/proxy/loop-prevention/tool-loop-guard.d.ts +1 -1
  36. package/dist/proxy/loop-prevention/tool-loop-guard.js +10 -0
  37. package/dist/proxy/orchestration/orchestrator.d.ts +1 -1
  38. package/dist/proxy/orchestration/semaphore.js +6 -0
  39. package/dist/proxy/patch/deepseek/index.d.ts +1 -1
  40. package/dist/proxy/patch/deepseek/patch-thinking-param.d.ts +1 -1
  41. package/dist/proxy/patch/index.d.ts +3 -0
  42. package/dist/proxy/patch/index.js +28 -0
  43. package/dist/proxy/patch/tool-round-limiter.d.ts +1 -1
  44. package/dist/proxy/patch/tool-round-limiter.js +16 -0
  45. package/dist/proxy/proxy-core.d.ts +1 -1
  46. package/dist/proxy/proxy-logging.d.ts +3 -3
  47. package/dist/proxy/response-transform.js +13 -0
  48. package/dist/proxy/transform/id-utils.d.ts +1 -0
  49. package/dist/proxy/transform/id-utils.js +3 -0
  50. package/dist/proxy/transform/plugin-types.d.ts +5 -5
  51. package/dist/proxy/transform/request-bridge-responses.d.ts +19 -0
  52. package/dist/proxy/transform/request-bridge-responses.js +311 -0
  53. package/dist/proxy/transform/request-transform-responses.d.ts +2 -0
  54. package/dist/proxy/transform/request-transform-responses.js +350 -0
  55. package/dist/proxy/transform/response-bridge-responses.d.ts +23 -0
  56. package/dist/proxy/transform/response-bridge-responses.js +173 -0
  57. package/dist/proxy/transform/response-transform-responses.d.ts +2 -0
  58. package/dist/proxy/transform/response-transform-responses.js +137 -0
  59. package/dist/proxy/transform/stream-ant2resp.d.ts +26 -0
  60. package/dist/proxy/transform/stream-ant2resp.js +322 -0
  61. package/dist/proxy/transform/stream-bridge-chat2resp.d.ts +40 -0
  62. package/dist/proxy/transform/stream-bridge-chat2resp.js +382 -0
  63. package/dist/proxy/transform/stream-bridge-resp2chat.d.ts +24 -0
  64. package/dist/proxy/transform/stream-bridge-resp2chat.js +237 -0
  65. package/dist/proxy/transform/stream-resp2ant.d.ts +21 -0
  66. package/dist/proxy/transform/stream-resp2ant.js +238 -0
  67. package/dist/proxy/transform/stream-transform-base.d.ts +1 -0
  68. package/dist/proxy/transform/stream-transform-base.js +3 -0
  69. package/dist/proxy/transform/transform-coordinator.d.ts +1 -0
  70. package/dist/proxy/transform/transform-coordinator.js +127 -8
  71. package/dist/proxy/transform/types-responses.d.ts +177 -0
  72. package/dist/proxy/transform/types-responses.js +27 -0
  73. package/dist/proxy/transform/types.d.ts +3 -1
  74. package/dist/proxy/transport/transport-fn.d.ts +1 -1
  75. package/frontend-dist/assets/CardContent-BhMXx-JD.js +1 -0
  76. package/frontend-dist/assets/CardTitle-DQDjTee3.js +1 -0
  77. package/frontend-dist/assets/CascadingModelSelect-JBQq3JJt.js +1 -0
  78. package/frontend-dist/assets/Checkbox-ByxbKP_C.js +1 -0
  79. package/frontend-dist/assets/CollapsibleContent-GecW2Jk_.js +1 -0
  80. package/frontend-dist/assets/CollapsibleTrigger-Cib3-OsK.js +1 -0
  81. package/frontend-dist/assets/Collection-Dbvdpa0m.js +1 -0
  82. package/frontend-dist/assets/Dashboard-3MJPLflT.js +3 -0
  83. package/frontend-dist/assets/DialogTitle-Ej_rtfV1.js +1 -0
  84. package/frontend-dist/assets/{Input-CAnKUBBK.js → Input-tcnrMp1v.js} +1 -1
  85. package/frontend-dist/assets/Label-BwzPFyL-.js +1 -0
  86. package/frontend-dist/assets/Login-Cdsw2pWC.js +1 -0
  87. package/frontend-dist/assets/Logs-5_CWiws5.js +1 -0
  88. package/frontend-dist/assets/MappingList-D8HRph05.js +1 -0
  89. package/frontend-dist/assets/ModelCard-CZbQcYNn.js +1 -0
  90. package/frontend-dist/assets/ModelMappings-CJqgl7O8.js +1 -0
  91. package/frontend-dist/assets/Monitor-B8v5a8fB.js +1 -0
  92. package/frontend-dist/assets/PopoverTrigger-C88SpJNZ.js +1 -0
  93. package/frontend-dist/assets/PopperContent-6BXua_FZ.js +1 -0
  94. package/frontend-dist/assets/Providers-DH0nvlGn.js +1 -0
  95. package/frontend-dist/assets/ProxyEnhancement-CAH-44W-.js +5 -0
  96. package/frontend-dist/assets/QuickSetup-CsDO-ZGP.js +1 -0
  97. package/frontend-dist/assets/RetryRules-8iT9fLsH.js +1 -0
  98. package/frontend-dist/assets/RouterKeys-BFoEmWgj.js +1 -0
  99. package/frontend-dist/assets/RovingFocusItem-DdPUFQHC.js +1 -0
  100. package/frontend-dist/assets/Schedules-B8Se31u4.js +1 -0
  101. package/frontend-dist/assets/SelectValue-CT2z_-6j.js +1 -0
  102. package/frontend-dist/assets/Settings-BHvtsJKD.js +6 -0
  103. package/frontend-dist/assets/Setup-k-l9KDC0.js +1 -0
  104. package/frontend-dist/assets/Switch-D1NdA4ax.js +1 -0
  105. package/frontend-dist/assets/TableHeader-CcMyOsUB.js +1 -0
  106. package/frontend-dist/assets/Teleport-Bmeh33lB.js +3 -0
  107. package/frontend-dist/assets/TooltipTrigger-LegC_Uvp.js +1 -0
  108. package/frontend-dist/assets/UnifiedRequestDialog-BVw6W2pk.js +3 -0
  109. package/frontend-dist/assets/UnifiedRequestDialog-C4MTxb25.css +1 -0
  110. package/frontend-dist/assets/VisuallyHidden-ogESfc9X.js +1 -0
  111. package/frontend-dist/assets/VisuallyHiddenInput-BQemVGau.js +1 -0
  112. package/frontend-dist/assets/alert-dialog-DzKCAoYJ.js +1 -0
  113. package/frontend-dist/assets/badge-C-9zPTgw.js +1 -0
  114. package/frontend-dist/assets/button-D27ClX8J.js +14 -0
  115. package/frontend-dist/assets/check-yTAivq1h.js +1 -0
  116. package/frontend-dist/assets/common-CWCbKHOK.js +1 -0
  117. package/frontend-dist/assets/common-D4xnnaqi.js +1 -0
  118. package/frontend-dist/assets/constants-B-VELBjk.js +1 -0
  119. package/frontend-dist/assets/copy-DWG9cQPR.js +1 -0
  120. package/frontend-dist/assets/dashboard-B8eI-t8c.js +1 -0
  121. package/frontend-dist/assets/dashboard-Dbe6A2lu.js +1 -0
  122. package/frontend-dist/assets/dialog-BnYR6_dh.js +1 -0
  123. package/frontend-dist/assets/file-text-D33FJAPX.js +1 -0
  124. package/frontend-dist/assets/format-BhxQSgt6.js +1 -0
  125. package/frontend-dist/assets/i18n-CwUfS0tE.js +1 -0
  126. package/frontend-dist/assets/index-B348nt-T.css +1 -0
  127. package/frontend-dist/assets/index-DPRxBo3N.js +1 -0
  128. package/frontend-dist/assets/lib-D0Ek2pPZ.js +1 -0
  129. package/frontend-dist/assets/loader-circle-EpKC006I.js +1 -0
  130. package/frontend-dist/assets/login-BTolYxVI.js +1 -0
  131. package/frontend-dist/assets/login-w_ICpiU5.js +1 -0
  132. package/frontend-dist/assets/logs-7dT2uyMa.js +1 -0
  133. package/frontend-dist/assets/logs-_3w8tDQa.js +1 -0
  134. package/frontend-dist/assets/mappings-Bbn3r2uJ.js +1 -0
  135. package/frontend-dist/assets/mappings-CTZ-zb1x.js +1 -0
  136. package/frontend-dist/assets/monitor-DN5m5n_x.js +1 -0
  137. package/frontend-dist/assets/monitor-DysWEOtt.js +1 -0
  138. package/frontend-dist/assets/providers-C1gQGzwa.js +1 -0
  139. package/frontend-dist/assets/providers-CCfko___.js +1 -0
  140. package/frontend-dist/assets/proxyEnhancement-BItabyLo.js +1 -0
  141. package/frontend-dist/assets/proxyEnhancement-DeMb7wIE.js +1 -0
  142. package/frontend-dist/assets/quickSetup-C75HMC_z.js +1 -0
  143. package/frontend-dist/assets/quickSetup-DStZWiuf.js +1 -0
  144. package/frontend-dist/assets/requestDetail-BoaPEQs-.js +1 -0
  145. package/frontend-dist/assets/requestDetail-CM5kFgy6.js +1 -0
  146. package/frontend-dist/assets/retryRules-CIF37gOl.js +1 -0
  147. package/frontend-dist/assets/retryRules-o_D8S5gy.js +1 -0
  148. package/frontend-dist/assets/routerKeys-BAvjW0V8.js +1 -0
  149. package/frontend-dist/assets/routerKeys-mQt2YPuE.js +1 -0
  150. package/frontend-dist/assets/schedules-BCV2rxK-.js +1 -0
  151. package/frontend-dist/assets/schedules-Qte9b7b_.js +1 -0
  152. package/frontend-dist/assets/settings-Bgu2lJfy.js +1 -0
  153. package/frontend-dist/assets/settings-UCmMSq_F.js +1 -0
  154. package/frontend-dist/assets/setup-B_fAfMoV.js +1 -0
  155. package/frontend-dist/assets/setup-Chc246Zi.js +1 -0
  156. package/frontend-dist/assets/sidebar-B7rejnZA.js +1 -0
  157. package/frontend-dist/assets/sidebar-CBMItLst.js +1 -0
  158. package/frontend-dist/assets/sun-BylRZIWt.js +1 -0
  159. package/frontend-dist/assets/trash-2-QNFff7V4.js +1 -0
  160. package/frontend-dist/assets/{useClipboard-BmmsNSGV.js → useClipboard-BFt5f-_-.js} +1 -1
  161. package/frontend-dist/assets/{useFocusGuards-A-9V2Y-b.js → useFocusGuards-DQBZKWnu.js} +1 -1
  162. package/frontend-dist/assets/useFormControl-T2RQNBqs.js +1 -0
  163. package/frontend-dist/assets/useLogRetention-NrrZrpPE.js +1 -0
  164. package/frontend-dist/assets/useNonce-DR38uny5.js +1 -0
  165. package/frontend-dist/assets/useTheme-CpTI547G.js +1 -0
  166. package/frontend-dist/assets/x-DSgLgKC_.js +1 -0
  167. package/frontend-dist/index.html +25 -22
  168. package/package.json +1 -1
  169. package/dist/db/migrations/033_add_pipeline_snapshot.sql +0 -1
  170. package/frontend-dist/assets/CardContent-BVMQ2_pg.js +0 -1
  171. package/frontend-dist/assets/CardTitle-GLv7QyIY.js +0 -1
  172. package/frontend-dist/assets/CascadingModelSelect-CBhqKFDX.js +0 -1
  173. package/frontend-dist/assets/Checkbox-HPVDmEdV.js +0 -1
  174. package/frontend-dist/assets/CollapsibleTrigger-DhxD9tpM.js +0 -1
  175. package/frontend-dist/assets/Collection-BRt7YxN8.js +0 -1
  176. package/frontend-dist/assets/Dashboard-D1Ys8Zog.js +0 -3
  177. package/frontend-dist/assets/DialogTitle-23q73lwF.js +0 -1
  178. package/frontend-dist/assets/Label-DWdYtVMI.js +0 -1
  179. package/frontend-dist/assets/Login-w5WFOinP.js +0 -1
  180. package/frontend-dist/assets/Logs-C1F1ZmWF.js +0 -1
  181. package/frontend-dist/assets/ModelMappings-BzmecWEH.js +0 -1
  182. package/frontend-dist/assets/Monitor-DrAZFTKR.js +0 -1
  183. package/frontend-dist/assets/PopoverTrigger-Bj65uUbv.js +0 -1
  184. package/frontend-dist/assets/PopperContent-gzzf1XHe.js +0 -1
  185. package/frontend-dist/assets/Providers-DSgf4mb6.js +0 -1
  186. package/frontend-dist/assets/ProxyEnhancement-Bb1cCP6d.js +0 -5
  187. package/frontend-dist/assets/RetryRules-BwPfEZtm.js +0 -1
  188. package/frontend-dist/assets/RouterKeys-CzTSq1Mx.js +0 -1
  189. package/frontend-dist/assets/RovingFocusItem-CXM_Yfkm.js +0 -1
  190. package/frontend-dist/assets/Schedules-DVilCXrC.js +0 -1
  191. package/frontend-dist/assets/SelectValue-C0-LzGQY.js +0 -1
  192. package/frontend-dist/assets/Settings-Bpk53zVX.js +0 -6
  193. package/frontend-dist/assets/Setup-Dn7EgC49.js +0 -1
  194. package/frontend-dist/assets/Switch-BO8Ooae6.js +0 -1
  195. package/frontend-dist/assets/TableHeader-Bded9VTC.js +0 -1
  196. package/frontend-dist/assets/TabsTrigger-BzKMi9AF.js +0 -1
  197. package/frontend-dist/assets/Teleport-DizRK5O3.js +0 -3
  198. package/frontend-dist/assets/TooltipTrigger-EiIy2zn8.js +0 -1
  199. package/frontend-dist/assets/UnifiedRequestDialog-BABsTaGb.js +0 -3
  200. package/frontend-dist/assets/UnifiedRequestDialog-BjEigSaR.css +0 -1
  201. package/frontend-dist/assets/VisuallyHidden-5AozJQza.js +0 -1
  202. package/frontend-dist/assets/VisuallyHiddenInput-DdiZrV2i.js +0 -1
  203. package/frontend-dist/assets/alert-dialog-DlKUuTPe.js +0 -1
  204. package/frontend-dist/assets/arrow-down-CxWKmZ2I.js +0 -1
  205. package/frontend-dist/assets/badge-9KJEMa53.js +0 -1
  206. package/frontend-dist/assets/button-Ul8WlrM5.js +0 -12
  207. package/frontend-dist/assets/check-7ahK--N4.js +0 -1
  208. package/frontend-dist/assets/constants-D_0jiLjw.js +0 -1
  209. package/frontend-dist/assets/copy-DzU2pAMG.js +0 -1
  210. package/frontend-dist/assets/dialog-B9j-FMrd.js +0 -1
  211. package/frontend-dist/assets/file-text-Bj3ZIo-E.js +0 -1
  212. package/frontend-dist/assets/format-Dln15Luw.js +0 -1
  213. package/frontend-dist/assets/index-Bz_ZaXNn.css +0 -1
  214. package/frontend-dist/assets/index-MedWZMHB.js +0 -1
  215. package/frontend-dist/assets/lib-Hhs3NqfD.js +0 -1
  216. package/frontend-dist/assets/loader-circle-5TJUukEe.js +0 -1
  217. package/frontend-dist/assets/useFormControl-DEO19lRe.js +0 -1
  218. package/frontend-dist/assets/useLogRetention-BfnBFZ5K.js +0 -1
  219. package/frontend-dist/assets/useNonce-BfwUJ1Ci.js +0 -1
  220. package/frontend-dist/assets/x-Cfopt3QL.js +0 -1
  221. /package/dist/db/migrations/{034_drop_redundant_log_columns.sql → 035_drop_redundant_log_columns.sql} +0 -0
  222. /package/frontend-dist/assets/{ohash.D__AXeF1-D5e5Wyzx.js → ohash.D__AXeF1-CTo5WcIm.js} +0 -0
@@ -0,0 +1,169 @@
1
+ import { Type } from "@sinclair/typebox";
2
+ import { createProvider } from "../db/providers.js";
3
+ import { createMappingGroup, updateMappingGroup } from "../db/mappings.js";
4
+ import { createRetryRule } from "../db/retry-rules.js";
5
+ import { upsertTransformRule } from "../db/transform-rules.js";
6
+ import { encrypt } from "../utils/crypto.js";
7
+ import { getSetting } from "../db/settings.js";
8
+ import { HTTP_CREATED, HTTP_BAD_REQUEST, HTTP_CONFLICT } from "./constants.js";
9
+ import { API_CODE, apiError } from "./api-response.js";
10
+ import { PROVIDER_CONCURRENCY_DEFAULTS } from "../db/providers.js";
11
+ const PROVIDER_NAME_RE = /^[a-zA-Z0-9_-]+$/;
12
+ const API_KEY_PREVIEW_MIN_LENGTH = 8;
13
+ const API_KEY_PREVIEW_PREFIX_LEN = 4;
14
+ const QuickSetupProviderSchema = Type.Object({
15
+ name: Type.String({ minLength: 1 }),
16
+ api_type: Type.Union([Type.Literal("openai"), Type.Literal("openai-responses"), Type.Literal("anthropic")]),
17
+ base_url: Type.String({ minLength: 1 }),
18
+ api_key: Type.String({ minLength: 1 }),
19
+ models: Type.Array(Type.Object({
20
+ name: Type.String(),
21
+ context_window: Type.Optional(Type.Number()),
22
+ patches: Type.Optional(Type.Array(Type.String())),
23
+ })),
24
+ concurrency_mode: Type.Optional(Type.Union([Type.Literal("auto"), Type.Literal("manual"), Type.Literal("none")])),
25
+ max_concurrency: Type.Optional(Type.Number()),
26
+ queue_timeout_ms: Type.Optional(Type.Number()),
27
+ max_queue_size: Type.Optional(Type.Number()),
28
+ });
29
+ const QuickSetupMappingSchema = Type.Object({
30
+ client_model: Type.String({ minLength: 1 }),
31
+ backend_model: Type.String({ minLength: 1 }),
32
+ });
33
+ const QuickSetupRetryRuleSchema = Type.Object({
34
+ name: Type.String({ minLength: 1 }),
35
+ status_code: Type.Number({ minimum: 100, maximum: 599 }),
36
+ body_pattern: Type.String({ minLength: 1 }),
37
+ retry_strategy: Type.Union([Type.Literal("fixed"), Type.Literal("exponential")]),
38
+ retry_delay_ms: Type.Number({ minimum: 100 }),
39
+ max_retries: Type.Number({ minimum: 0, maximum: 100 }),
40
+ max_delay_ms: Type.Number({ minimum: 100 }),
41
+ });
42
+ const QuickSetupTransformSchema = Type.Object({
43
+ inject_headers: Type.Optional(Type.Record(Type.String(), Type.String())),
44
+ request_defaults: Type.Optional(Type.Record(Type.String(), Type.Unknown())),
45
+ drop_fields: Type.Optional(Type.Array(Type.String())),
46
+ });
47
+ const QuickSetupSchema = Type.Object({
48
+ provider: QuickSetupProviderSchema,
49
+ mappings: Type.Array(QuickSetupMappingSchema),
50
+ retry_rules: Type.Array(QuickSetupRetryRuleSchema),
51
+ transform_rules: Type.Optional(QuickSetupTransformSchema),
52
+ });
53
+ export const adminQuickSetupRoutes = (app, options, done) => {
54
+ const { db, stateRegistry, tracker, adaptiveController } = options;
55
+ app.post("/admin/api/quick-setup", { schema: { body: QuickSetupSchema } }, async (request, reply) => {
56
+ const body = request.body;
57
+ // 1. Validate provider name
58
+ if (!PROVIDER_NAME_RE.test(body.provider.name)) {
59
+ return reply.code(HTTP_BAD_REQUEST).send(apiError(API_CODE.VALIDATION_FAILED, "Provider 名称仅允许英文大小写字母、数字、横线和下划线"));
60
+ }
61
+ // 2. Check no duplicate provider name
62
+ const existing = db.prepare("SELECT id FROM providers WHERE name = ?").get(body.provider.name);
63
+ if (existing) {
64
+ return reply.code(HTTP_CONFLICT).send(apiError(API_CODE.CONFLICT_NAME, `Provider 名称 '${body.provider.name}' 已存在`));
65
+ }
66
+ // 3. Validate retry rule body_pattern regex
67
+ for (const rule of body.retry_rules) {
68
+ try {
69
+ new RegExp(rule.body_pattern);
70
+ }
71
+ catch {
72
+ return reply.code(HTTP_BAD_REQUEST).send(apiError(API_CODE.INVALID_REGEX, `重试规则「${rule.name}」的 body_pattern 不是有效的正则表达式`));
73
+ }
74
+ }
75
+ // 4. Start transaction
76
+ const encryptionKey = getSetting(db, "encryption_key");
77
+ const createAll = db.transaction(() => {
78
+ // 5. Create provider with models JSON
79
+ const encryptedKey = encrypt(body.provider.api_key, encryptionKey);
80
+ const modelEntries = body.provider.models.map(m => ({
81
+ name: m.name,
82
+ ...(m.context_window != null ? { context_window: m.context_window } : {}),
83
+ ...(m.patches && m.patches.length > 0 ? { patches: m.patches } : {}),
84
+ }));
85
+ const adaptiveEnabled = body.provider.concurrency_mode === 'auto' ? 1 : 0;
86
+ const maxConcurrency = body.provider.max_concurrency ?? PROVIDER_CONCURRENCY_DEFAULTS.max_concurrency;
87
+ const queueTimeoutMs = body.provider.queue_timeout_ms ?? PROVIDER_CONCURRENCY_DEFAULTS.queue_timeout_ms;
88
+ const maxQueueSize = body.provider.max_queue_size ?? PROVIDER_CONCURRENCY_DEFAULTS.max_queue_size;
89
+ const providerId = createProvider(db, {
90
+ name: body.provider.name,
91
+ api_type: body.provider.api_type,
92
+ base_url: body.provider.base_url,
93
+ api_key: encryptedKey,
94
+ api_key_preview: body.provider.api_key.length > API_KEY_PREVIEW_MIN_LENGTH
95
+ ? `${body.provider.api_key.slice(0, API_KEY_PREVIEW_PREFIX_LEN)}...${body.provider.api_key.slice(-API_KEY_PREVIEW_PREFIX_LEN)}`
96
+ : "****",
97
+ models: JSON.stringify(modelEntries),
98
+ is_active: 1,
99
+ max_concurrency: maxConcurrency,
100
+ queue_timeout_ms: queueTimeoutMs,
101
+ max_queue_size: maxQueueSize,
102
+ adaptive_enabled: adaptiveEnabled,
103
+ });
104
+ // 6. Upsert mapping groups
105
+ for (const m of body.mappings) {
106
+ const existing = db.prepare('SELECT id FROM mapping_groups WHERE client_model = ?').get(m.client_model);
107
+ const ruleJson = JSON.stringify({
108
+ targets: [{ backend_model: m.backend_model, provider_id: providerId }],
109
+ });
110
+ if (existing) {
111
+ updateMappingGroup(db, existing.id, {
112
+ client_model: m.client_model,
113
+ rule: ruleJson,
114
+ });
115
+ }
116
+ else {
117
+ createMappingGroup(db, {
118
+ client_model: m.client_model,
119
+ rule: ruleJson,
120
+ });
121
+ }
122
+ }
123
+ // 7. Create retry rules
124
+ for (const r of body.retry_rules) {
125
+ createRetryRule(db, {
126
+ name: r.name,
127
+ status_code: r.status_code,
128
+ body_pattern: r.body_pattern,
129
+ is_active: 1,
130
+ retry_strategy: r.retry_strategy,
131
+ retry_delay_ms: r.retry_delay_ms,
132
+ max_retries: r.max_retries,
133
+ max_delay_ms: r.max_delay_ms,
134
+ });
135
+ }
136
+ // 8. Create transform rules
137
+ if (body.transform_rules) {
138
+ upsertTransformRule(db, providerId, {
139
+ inject_headers: body.transform_rules.inject_headers ?? null,
140
+ request_defaults: body.transform_rules.request_defaults ?? null,
141
+ drop_fields: body.transform_rules.drop_fields ?? null,
142
+ is_active: 1,
143
+ });
144
+ }
145
+ return providerId;
146
+ });
147
+ // 8. Execute transaction
148
+ const providerId = createAll();
149
+ // 9. Sync concurrency state
150
+ const finalAdaptiveEnabled = body.provider.concurrency_mode === 'auto' ? 1 : 0;
151
+ const finalMaxConcurrency = body.provider.max_concurrency ?? PROVIDER_CONCURRENCY_DEFAULTS.max_concurrency;
152
+ const finalQueueTimeoutMs = body.provider.queue_timeout_ms ?? PROVIDER_CONCURRENCY_DEFAULTS.queue_timeout_ms;
153
+ const finalMaxQueueSize = body.provider.max_queue_size ?? PROVIDER_CONCURRENCY_DEFAULTS.max_queue_size;
154
+ adaptiveController?.syncProvider(providerId, {
155
+ adaptive_enabled: finalAdaptiveEnabled,
156
+ max_concurrency: finalMaxConcurrency,
157
+ queue_timeout_ms: finalQueueTimeoutMs,
158
+ max_queue_size: finalMaxQueueSize,
159
+ });
160
+ tracker?.updateProviderConfig(providerId, {
161
+ name: body.provider.name,
162
+ maxConcurrency: finalMaxConcurrency,
163
+ queueTimeoutMs: finalQueueTimeoutMs,
164
+ maxQueueSize: finalMaxQueueSize,
165
+ });
166
+ return reply.code(HTTP_CREATED).send({ success: true, provider_id: providerId });
167
+ });
168
+ done();
169
+ };
@@ -7,7 +7,11 @@ export const adminRecommendedRoutes = (app, options, done) => {
7
7
  app.get("/admin/api/recommended/retry-rules", async (_req, reply) => {
8
8
  const rules = getRecommendedRetryRules();
9
9
  const existing = new Set(db.prepare("SELECT name FROM retry_rules").all().map((r) => r.name));
10
- return reply.send(rules.filter((r) => !existing.has(r.name)));
10
+ // Return all rules with `exists` flag, so the frontend can show all and mark existing ones
11
+ return reply.send(rules.map(r => ({
12
+ ...r,
13
+ exists: existing.has(r.name),
14
+ })));
11
15
  });
12
16
  app.post("/admin/api/recommended/reload", async (_req, reply) => {
13
17
  reloadConfig();
@@ -14,6 +14,7 @@ import { adminSettingsRoutes } from "./settings.js";
14
14
  import { adminRecommendedRoutes } from "./recommended.js";
15
15
  import { adminUsageRoutes } from "./usage.js";
16
16
  import { adminUpgradeRoutes } from "./upgrade.js";
17
+ import { adminQuickSetupRoutes } from "./quick-setup.js";
17
18
  import { adminImportExportRoutes } from "./settings-import-export.js";
18
19
  import { adminTransformRuleRoutes } from "./transform-rules.js";
19
20
  import { adminScheduleRoutes } from "./schedules.js";
@@ -37,6 +38,7 @@ export const adminRoutes = (app, options, done) => {
37
38
  app.register(adminImportExportRoutes, { db: options.db, stateRegistry: options.stateRegistry, pluginRegistry: options.pluginRegistry });
38
39
  app.register(adminRecommendedRoutes, { db: options.db });
39
40
  app.register(adminUsageRoutes, { db: options.db });
41
+ app.register(adminQuickSetupRoutes, { db: options.db, stateRegistry: options.stateRegistry, tracker: options.tracker, adaptiveController: options.adaptiveController });
40
42
  app.register(adminUpgradeRoutes, { db: options.db, closeFn: options.closeFn ?? (async () => { }) });
41
43
  app.register(adminTransformRuleRoutes, { db: options.db, pluginRegistry: options.pluginRegistry });
42
44
  done();
@@ -1,10 +1,16 @@
1
1
  export interface ModelInfo {
2
2
  name: string;
3
3
  context_window: number | null;
4
+ patches: string[];
5
+ }
6
+ export interface ModelEntry {
7
+ name: string;
8
+ context_window?: number;
9
+ patches?: string[];
4
10
  }
5
11
  export declare const MODEL_CONTEXT_WINDOWS: Record<string, number>;
6
12
  export declare const DEFAULT_CONTEXT_WINDOW = 200000;
7
13
  export declare const OVERFLOW_THRESHOLD = 1000000;
8
14
  export declare function lookupContextWindow(modelName: string): number;
9
- export declare function parseModels(raw: string): string[];
10
- export declare function buildModelInfoList(modelNames: string[], overrides: Map<string, number>): ModelInfo[];
15
+ export declare function parseModels(raw: string): ModelEntry[];
16
+ export declare function buildModelInfoList(modelEntries: ModelEntry[], overrides: Map<string, number>): ModelInfo[];
@@ -91,15 +91,27 @@ export function parseModels(raw) {
91
91
  const parsed = JSON.parse(raw);
92
92
  if (!Array.isArray(parsed))
93
93
  return [];
94
- return parsed.map((item) => typeof item === 'string' ? item : item?.name ?? '').filter(Boolean);
94
+ return parsed.map((item) => {
95
+ if (typeof item === 'string') {
96
+ return item ? { name: item, patches: [] } : null;
97
+ }
98
+ const obj = item;
99
+ if (!obj || !obj.name)
100
+ return null;
101
+ return {
102
+ name: obj.name,
103
+ patches: obj.patches ?? [],
104
+ };
105
+ }).filter((e) => e !== null);
95
106
  }
96
107
  catch {
97
108
  return [];
98
109
  }
99
110
  }
100
- export function buildModelInfoList(modelNames, overrides) {
101
- return modelNames.map(name => ({
102
- name,
103
- context_window: overrides.get(name) ?? lookupContextWindow(name),
111
+ export function buildModelInfoList(modelEntries, overrides) {
112
+ return modelEntries.map(entry => ({
113
+ name: entry.name,
114
+ context_window: overrides.get(entry.name) ?? lookupContextWindow(entry.name),
115
+ patches: entry.patches ?? [],
104
116
  }));
105
117
  }
@@ -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
  }
@@ -17,6 +17,7 @@ export interface RecommendedRetryRule {
17
17
  retry_delay_ms: number;
18
18
  max_retries: number;
19
19
  max_delay_ms: number;
20
+ providers?: string[];
20
21
  }
21
22
  export declare function loadRecommendedConfig(dir?: string): void;
22
23
  export declare function getRecommendedProviders(): ProviderGroup[];
@@ -1,22 +1,18 @@
1
1
  import fs from 'fs';
2
2
  import path from 'path';
3
- let cachedProviders = [];
4
- let cachedRetryRules = [];
5
3
  let configDir = '';
6
4
  export function loadRecommendedConfig(dir) {
7
5
  configDir = dir ?? path.resolve(process.cwd(), 'config');
8
- cachedProviders = loadJson('recommended-providers.json');
9
- cachedRetryRules = loadJson('recommended-retry-rules.json');
10
6
  }
11
7
  export function getRecommendedProviders() {
12
- return cachedProviders;
8
+ return loadJson('recommended-providers.json');
13
9
  }
14
10
  export function getRecommendedRetryRules() {
15
- return cachedRetryRules;
16
- }
17
- export function reloadConfig() {
18
- loadRecommendedConfig(configDir);
11
+ return loadJson('recommended-retry-rules.json');
19
12
  }
13
+ // No-op: kept for backward compat (reload endpoint, upgrade flow)
14
+ // Config is now always read from disk, no caching.
15
+ export function reloadConfig() { }
20
16
  function loadJson(filename) {
21
17
  const filePath = path.join(configDir, filename);
22
18
  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
  }
@@ -32,6 +32,9 @@ export class MetricsExtractor {
32
32
  if (this.apiType === "anthropic") {
33
33
  this.processAnthropicEvent(event);
34
34
  }
35
+ else if (this.apiType === "openai-responses") {
36
+ this.processResponsesEvent(event);
37
+ }
35
38
  else {
36
39
  this.processOpenAIEvent(event);
37
40
  }
@@ -118,6 +121,48 @@ export class MetricsExtractor {
118
121
  }
119
122
  return extractAnthropicNonStream(parsed);
120
123
  }
124
+ processResponsesEvent(event) {
125
+ let obj;
126
+ try {
127
+ obj = JSON.parse(event.data);
128
+ }
129
+ catch {
130
+ return;
131
+ }
132
+ const type = obj.type;
133
+ // Track first content for TTFT
134
+ const isContentDelta = type === "response.output_text.delta"
135
+ || type === "response.function_call_arguments.delta"
136
+ || type === "response.reasoning_summary_text.delta";
137
+ if (isContentDelta) {
138
+ const delta = obj.delta ?? "";
139
+ if (delta && !this.firstContentReceived) {
140
+ this.firstContentReceived = true;
141
+ this.ttftMs = Date.now() - this.requestStartTime;
142
+ }
143
+ this.textContentBuffer += delta;
144
+ }
145
+ // Track completion
146
+ if (type === "response.completed" || type === "response.incomplete") {
147
+ this.streamEndTime = Date.now();
148
+ this.complete = true;
149
+ const resp = obj.response;
150
+ if (resp) {
151
+ const usage = resp.usage;
152
+ if (usage) {
153
+ this.inputTokens = usage.input_tokens ?? null;
154
+ this.outputTokens = usage.output_tokens ?? null;
155
+ }
156
+ const status = resp.status;
157
+ if (status === "completed")
158
+ this.stopReason = "end_turn";
159
+ else if (status === "incomplete")
160
+ this.stopReason = "max_tokens";
161
+ else
162
+ this.stopReason = "stop";
163
+ }
164
+ }
165
+ }
121
166
  processAnthropicEvent(event) {
122
167
  let parsed;
123
168
  try {
@@ -27,7 +27,7 @@ export declare class SSEMetricsTransform extends Transform {
27
27
  private throttleMs;
28
28
  private lastCallbackTime;
29
29
  private flushed;
30
- constructor(apiType: "openai" | "anthropic", requestStartTime: number, options?: MetricsTransformOptions);
30
+ constructor(apiType: "openai" | "openai-responses" | "anthropic", requestStartTime: number, options?: MetricsTransformOptions);
31
31
  _transform(chunk: Buffer, _encoding: BufferEncoding, callback: TransformCallback): void;
32
32
  _flush(callback: TransformCallback): void;
33
33
  getExtractor(): MetricsExtractor;
@@ -83,6 +83,16 @@ export class SSEMetricsTransform extends Transform {
83
83
  if (delta.type === "input_json_delta" && typeof delta.partial_json === "string")
84
84
  return delta.partial_json;
85
85
  }
86
+ else if (this.apiType === "openai-responses") {
87
+ const type = parsed.type;
88
+ if (type === "response.output_text.delta")
89
+ return parsed.delta;
90
+ if (type === "response.reasoning_summary_text.delta")
91
+ return parsed.delta;
92
+ if (type === "response.function_call_arguments.delta")
93
+ return parsed.delta;
94
+ return undefined;
95
+ }
86
96
  else {
87
97
  const choices = parsed.choices;
88
98
  if (!Array.isArray(choices) || choices.length === 0)
@@ -34,7 +34,7 @@ export declare class RequestTracker {
34
34
  private scheduleStreamContentPush;
35
35
  private flushStreamContentPush;
36
36
  update(id: string, patch: Partial<ActiveRequest>): void;
37
- appendStreamChunk(id: string, rawLine: string, apiType: "openai" | "anthropic", maxRaw: number, maxText: number): void;
37
+ appendStreamChunk(id: string, rawLine: string, apiType: "openai" | "openai-responses" | "anthropic", maxRaw: number, maxText: number): void;
38
38
  complete(id: string, result: {
39
39
  status: "completed" | "failed";
40
40
  statusCode?: number;
@@ -9,6 +9,6 @@ export declare class StreamContentAccumulator {
9
9
  private totalChars;
10
10
  private blocks;
11
11
  constructor(maxRaw?: number, maxText?: number);
12
- append(rawLine: string, apiType: "openai" | "anthropic"): void;
12
+ append(rawLine: string, apiType: "openai" | "openai-responses" | "anthropic"): void;
13
13
  getSnapshot(): StreamContentSnapshot;
14
14
  }
@@ -8,4 +8,4 @@ export interface StreamExtraction {
8
8
  name?: string;
9
9
  } | null;
10
10
  }
11
- export declare function extractStreamText(line: string, apiType: "openai" | "anthropic"): StreamExtraction;
11
+ export declare function extractStreamText(line: string, apiType: "openai" | "openai-responses" | "anthropic"): StreamExtraction;