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
@@ -19,6 +19,27 @@ export function extractStreamText(line, apiType) {
19
19
  const text = delta?.content ?? "";
20
20
  return { text, block: text ? { index: 0, type: "text", content: text } : null };
21
21
  }
22
+ if (apiType === "openai-responses") {
23
+ // Responses SSE uses named events, but line format is "data: {json}" (same as Anthropic)
24
+ // The event type is in the data JSON's "type" field
25
+ const type = obj.type;
26
+ if (type === "response.output_text.delta") {
27
+ const text = obj.delta ?? "";
28
+ const outputIndex = obj.output_index ?? 0;
29
+ return { text, block: text ? { index: outputIndex, type: "text", content: text } : empty.block };
30
+ }
31
+ if (type === "response.function_call_arguments.delta") {
32
+ const partialJson = obj.delta ?? "";
33
+ const outputIndex = obj.output_index ?? 0;
34
+ return { text: "", block: { index: outputIndex, type: "tool_use", content: partialJson } };
35
+ }
36
+ if (type === "response.reasoning_summary_text.delta") {
37
+ const thinking = obj.delta ?? "";
38
+ const outputIndex = obj.output_index ?? 0;
39
+ return { text: "", block: { index: outputIndex, type: "thinking", content: thinking } };
40
+ }
41
+ return empty;
42
+ }
22
43
  // Anthropic
23
44
  const type = obj.type;
24
45
  const index = obj.index;
@@ -14,7 +14,7 @@ export interface StreamContentSnapshot {
14
14
  }
15
15
  export interface ActiveRequest {
16
16
  id: string;
17
- apiType: "openai" | "anthropic";
17
+ apiType: "openai" | "openai-responses" | "anthropic";
18
18
  model: string;
19
19
  providerId: string;
20
20
  providerName: string;
@@ -4,6 +4,6 @@ import type { TransportResult } from "../types.js";
4
4
  /** 从 TransportResult 中提取最终 HTTP status code */
5
5
  export declare function getTransportStatusCode(result: TransportResult): number | null;
6
6
  /** 将 tracker blocks 序列化为前端 tryDirectParse 可解析的 JSON */
7
- export declare function serializeBlocksForStorage(blocks: ContentBlock[] | undefined, apiType: "openai" | "anthropic"): string;
7
+ export declare function serializeBlocksForStorage(blocks: ContentBlock[] | undefined, apiType: "openai" | "openai-responses" | "anthropic"): string;
8
8
  /** 从请求体中提取最后一次工具调用记录 */
9
9
  export declare function extractLastToolUse(body: Record<string, unknown>): ToolCallRecord | null;
@@ -8,6 +8,6 @@ export interface RouteHandlerDeps {
8
8
  container: ServiceContainer;
9
9
  }
10
10
  import type { ServiceContainer } from "../../core/container.js";
11
- export declare function handleProxyRequest(request: FastifyRequest, reply: FastifyReply, apiType: "openai" | "anthropic", upstreamPath: string, errors: ProxyErrorFormatter, deps: RouteHandlerDeps, options?: {
11
+ export declare function handleProxyRequest(request: FastifyRequest, reply: FastifyReply, apiType: "openai" | "openai-responses" | "anthropic", upstreamPath: string, errors: ProxyErrorFormatter, deps: RouteHandlerDeps, options?: {
12
12
  beforeSendProxy?: (body: Record<string, unknown>, isStream: boolean) => void;
13
13
  }): Promise<FastifyReply>;
@@ -13,6 +13,7 @@ import { insertRejectedLog } from "../log-helpers.js";
13
13
  import { ToolLoopGuard } from "../loop-prevention/tool-loop-guard.js";
14
14
  import { buildTransportFn } from "../transport/transport-fn.js";
15
15
  import { applyOverflowRedirect } from "../routing/overflow.js";
16
+ import { parseModels } from "../../config/model-context.js";
16
17
  import { applyProviderPatches } from "../patch/index.js";
17
18
  import { PipelineSnapshot } from "../pipeline-snapshot.js";
18
19
  import { maybeInjectModelInfoTag } from "../response-transform.js";
@@ -223,8 +224,13 @@ async function executeFailoverLoop(ctx) {
223
224
  pluginRegistry.applyAfterRequest(pluginCtx);
224
225
  injectedHeaders = pluginCtx.headers;
225
226
  }
226
- // provider patches — 使用返回值
227
- const { body: patchedBody, meta: patchMeta } = applyProviderPatches(currentBody, provider);
227
+ // provider patches — 优先从 DB models JSON 读取 patch 配置,无配置时回退自动检测
228
+ const providerModels = parseModels(provider.models || "[]");
229
+ const { body: patchedBody, meta: patchMeta } = applyProviderPatches(currentBody, {
230
+ base_url: provider.base_url,
231
+ api_type: provider.api_type,
232
+ models: providerModels,
233
+ });
228
234
  iterationSnapshot.add({ stage: "provider_patch", types: patchMeta.types });
229
235
  const encryptionKey = getSetting(deps.db, "encryption_key");
230
236
  if (!encryptionKey) {
@@ -0,0 +1,7 @@
1
+ import type { FastifyPluginCallback } from "fastify";
2
+ import Database from "better-sqlite3";
3
+ export interface ResponsesProxyOptions {
4
+ db: Database.Database;
5
+ container: import("../../core/container.js").ServiceContainer;
6
+ }
7
+ export declare const responsesProxy: FastifyPluginCallback<ResponsesProxyOptions>;
@@ -0,0 +1,48 @@
1
+ import { randomUUID } from "crypto";
2
+ import fp from "fastify-plugin";
3
+ import { insertRequestLog } from "../../db/index.js";
4
+ import { createErrorFormatter } from "../proxy-core.js";
5
+ import { handleProxyRequest } from "./proxy-handler.js";
6
+ import { createOrchestrator } from "../orchestration/orchestrator.js";
7
+ import { HTTP_BAD_GATEWAY } from "../../core/constants.js";
8
+ import { SERVICE_KEYS } from "../../core/container.js";
9
+ const RESPONSES_PATH = "/v1/responses";
10
+ const RESPONSES_COMPAT_PATH = "/responses";
11
+ const RESPONSES_ERROR_META = {
12
+ modelNotFound: { type: "invalid_request_error", code: "model_not_found" },
13
+ modelNotAllowed: { type: "invalid_request_error", code: "model_not_allowed" },
14
+ providerUnavailable: { type: "server_error", code: "provider_unavailable" },
15
+ providerTypeMismatch: { type: "server_error", code: "provider_type_mismatch" },
16
+ upstreamConnectionFailed: { type: "upstream_error", code: "upstream_connection_failed" },
17
+ concurrencyQueueFull: { type: "server_error", code: "concurrency_queue_full" },
18
+ concurrencyTimeout: { type: "server_error", code: "concurrency_timeout" },
19
+ promptTooLong: { type: "invalid_request_error", code: "context_window_exceeded" },
20
+ };
21
+ const responsesErrors = createErrorFormatter((kind, message) => ({ error: { message, ...RESPONSES_ERROR_META[kind] } }));
22
+ function sendError(reply, e) {
23
+ return reply.code(e.statusCode).send(e.body);
24
+ }
25
+ const responsesProxyRaw = (app, opts, done) => {
26
+ const { db, container } = opts;
27
+ const orchestrator = createOrchestrator(container.resolve(SERVICE_KEYS.semaphoreManager), container.resolve(SERVICE_KEYS.tracker), container.resolve(SERVICE_KEYS.adaptiveController));
28
+ const handleResponses = async (request, reply) => {
29
+ if (!orchestrator) {
30
+ const body = request.body;
31
+ insertRequestLog(db, {
32
+ id: randomUUID(), api_type: "openai-responses", model: body?.model || null,
33
+ provider_id: null, status_code: HTTP_BAD_GATEWAY, latency_ms: 0, is_stream: 0,
34
+ error_message: "Orchestrator not available",
35
+ created_at: new Date().toISOString(),
36
+ client_request: JSON.stringify({ headers: request.headers }),
37
+ router_key_id: request.routerKey?.id ?? null,
38
+ });
39
+ return sendError(reply, responsesErrors.providerUnavailable());
40
+ }
41
+ const deps = { db, orchestrator, container };
42
+ return handleProxyRequest(request, reply, "openai-responses", RESPONSES_PATH, responsesErrors, deps);
43
+ };
44
+ app.post(RESPONSES_PATH, handleResponses);
45
+ app.post(RESPONSES_COMPAT_PATH, handleResponses);
46
+ done();
47
+ };
48
+ export const responsesProxy = fp(responsesProxyRaw, { name: "responses-proxy" });
@@ -9,5 +9,5 @@ export declare class ToolLoopGuard {
9
9
  * 如果 sessionKey 不可用(无 sessionId),返回 detected: false。
10
10
  */
11
11
  check(sessionKey: string | null, toolCall: ToolCallRecord | null): LoopCheckResult;
12
- injectLoopBreakPrompt(body: Record<string, unknown>, apiType: "openai" | "anthropic", toolName: string): Record<string, unknown>;
12
+ injectLoopBreakPrompt(body: Record<string, unknown>, apiType: "openai" | "openai-responses" | "anthropic", toolName: string): Record<string, unknown>;
13
13
  }
@@ -50,6 +50,16 @@ export class ToolLoopGuard {
50
50
  cloned.system = [{ type: "text", text: prompt }];
51
51
  }
52
52
  }
53
+ else if (apiType === "openai-responses") {
54
+ // Append a user message to input items
55
+ const inputArr = Array.isArray(body.input) ? [...body.input] : [];
56
+ inputArr.push({
57
+ type: "message",
58
+ role: "user",
59
+ content: [{ type: "input_text", text: `[系统提醒] 检测到工具 "${toolName}" 可能陷入循环。请停止重复调用,总结当前进展。` }],
60
+ });
61
+ return { ...body, input: inputArr };
62
+ }
53
63
  else {
54
64
  const messages = cloned.messages ?? [];
55
65
  messages.unshift({ role: "system", content: prompt });
@@ -52,7 +52,7 @@ export declare class ProxyOrchestrator {
52
52
  resilience: ResilienceLayer;
53
53
  adaptiveController?: AdaptiveConcurrencyController;
54
54
  });
55
- handle(request: FastifyRequest, reply: FastifyReply, apiType: "openai" | "anthropic", config: OrchestratorConfig, ctx?: HandleContext): Promise<ResilienceResult>;
55
+ handle(request: FastifyRequest, reply: FastifyReply, apiType: "openai" | "openai-responses" | "anthropic", config: OrchestratorConfig, ctx?: HandleContext): Promise<ResilienceResult>;
56
56
  private buildActiveRequest;
57
57
  private createAbortSignal;
58
58
  private executeResilience;
@@ -34,6 +34,12 @@ export class ProviderSemaphoreManager {
34
34
  }
35
35
  if (entry.current < 0)
36
36
  entry.current = 0;
37
+ // 当 maxConcurrency 降低时,将 current 限制在新的上限,
38
+ // 防止 current 停留在旧的高位,导致新请求始终进入队列、
39
+ // 且 release() 无法通过空队检查来递减 current(排队长驻)
40
+ if (entry.current > config.maxConcurrency) {
41
+ entry.current = config.maxConcurrency;
42
+ }
37
43
  while (entry.current < config.maxConcurrency &&
38
44
  entry.queue.length > 0) {
39
45
  entry.current++;
@@ -15,4 +15,4 @@
15
15
  * 1. patchNonDeepSeekToolMessages — 将非 DeepSeek 生成的 tool_calls 降级为 text
16
16
  * 2. patchOrphanToolResultsOA — 处理孤儿 tool 消息
17
17
  */
18
- export declare function applyDeepSeekPatches(body: Record<string, unknown>, apiType: "openai" | "anthropic"): void;
18
+ export declare function applyDeepSeekPatches(body: Record<string, unknown>, apiType: "openai" | "openai-responses" | "anthropic"): void;
@@ -3,4 +3,4 @@
3
3
  * 客户端(如 Claude Code)可能在后续轮次省略此参数。
4
4
  * 检测历史中是否存在 thinking 内容,自动补上参数。
5
5
  */
6
- export declare function patchThinkingParam(body: Record<string, unknown>, apiType: "openai" | "anthropic"): void;
6
+ export declare function patchThinkingParam(body: Record<string, unknown>, apiType: "openai" | "openai-responses" | "anthropic"): void;
@@ -1,12 +1,15 @@
1
+ import type { ModelEntry } from "../../config/model-context.js";
1
2
  export interface ProviderInfo {
2
3
  base_url: string;
3
4
  api_type: string;
5
+ models?: ModelEntry[];
4
6
  }
5
7
  export interface ProviderPatchMeta {
6
8
  types: string[];
7
9
  }
8
10
  /**
9
11
  * 根据 provider 信息分发到对应的补丁逻辑。
12
+ * 优先使用 DB 配置的 patches 模式,无配置时回退到自动检测。
10
13
  * 返回浅拷贝 body + 执行的补丁类型列表,不修改原始 body。
11
14
  */
12
15
  export declare function applyProviderPatches(body: Record<string, unknown>, provider: ProviderInfo): {
@@ -2,6 +2,7 @@ import { applyDeepSeekPatches } from "./deepseek/index.js";
2
2
  const OPENAI_ORIGIN_HOSTS = ["api.openai.com", "openai.com"];
3
3
  /**
4
4
  * 根据 provider 信息分发到对应的补丁逻辑。
5
+ * 优先使用 DB 配置的 patches 模式,无配置时回退到自动检测。
5
6
  * 返回浅拷贝 body + 执行的补丁类型列表,不修改原始 body。
6
7
  */
7
8
  export function applyProviderPatches(body, provider) {
@@ -15,6 +16,33 @@ export function applyProviderPatches(body, provider) {
15
16
  }
16
17
  return patched;
17
18
  };
19
+ // ---- DB-driven mode:通过 provider.models 配置的 patches 驱动 ----
20
+ if (provider.models) {
21
+ const modelName = body.model ?? "";
22
+ const modelEntry = provider.models.find(m => m.name === modelName);
23
+ const modelPatches = modelEntry?.patches ?? [];
24
+ if (modelPatches.length > 0) {
25
+ // developer_role 补丁(仅 openai 格式需要)
26
+ if (modelPatches.includes("developer_role") && provider.api_type === "openai" && hasDeveloperRole(body)) {
27
+ patchDeveloperRole(ensureCloned());
28
+ patches.push("developer_role");
29
+ }
30
+ // DeepSeek Anthropic 补丁
31
+ const dsAnthropicPatches = ["thinking-param", "cache-control", "thinking-blocks", "orphan-tool-results"];
32
+ if (dsAnthropicPatches.some(p => modelPatches.includes(p)) && provider.api_type === "anthropic") {
33
+ applyDeepSeekPatches(ensureCloned(), "anthropic");
34
+ patches.push("deepseek");
35
+ }
36
+ // DeepSeek OpenAI 补丁
37
+ const dsOpenAIPatches = ["non-ds-tools", "orphan-tool-results-oa"];
38
+ if (dsOpenAIPatches.some(p => modelPatches.includes(p)) && provider.api_type === "openai") {
39
+ applyDeepSeekPatches(ensureCloned(), "openai");
40
+ patches.push("deepseek");
41
+ }
42
+ return { body: patched ?? body, meta: { types: patches } };
43
+ }
44
+ }
45
+ // ---- 回退模式:自动检测(保持现有逻辑不变)----
18
46
  // 通用补丁:OpenAI 兼容 provider(非 OpenAI 原生)不支持 developer role
19
47
  if (provider.api_type === "openai" && !isOpenAIOrigin(provider.base_url)) {
20
48
  if (hasDeveloperRole(body)) {
@@ -30,7 +30,7 @@ export declare function countConsecutiveToolRounds(messages: Message[]): number;
30
30
  /**
31
31
  * 检测并注入提示词。返回可能修改后的 body(浅拷贝),未超阈值时原样返回。
32
32
  */
33
- export declare function applyToolRoundLimit(body: Record<string, unknown>, apiType: "openai" | "anthropic", maxRounds?: number): {
33
+ export declare function applyToolRoundLimit(body: Record<string, unknown>, apiType: "openai" | "openai-responses" | "anthropic", maxRounds?: number): {
34
34
  body: Record<string, unknown>;
35
35
  injected: boolean;
36
36
  rounds: number;
@@ -50,6 +50,22 @@ export function countConsecutiveToolRounds(messages) {
50
50
  * 检测并注入提示词。返回可能修改后的 body(浅拷贝),未超阈值时原样返回。
51
51
  */
52
52
  export function applyToolRoundLimit(body, apiType, maxRounds = DEFAULT_MAX_ROUNDS) {
53
+ if (apiType === "openai-responses") {
54
+ const input = body.input;
55
+ if (!input || !Array.isArray(input))
56
+ return { body, injected: false, rounds: 0 };
57
+ const funcCalls = input.filter(i => i.type === "function_call").length;
58
+ if (funcCalls <= maxRounds)
59
+ return { body, injected: false, rounds: funcCalls };
60
+ // Inject warning: append a user message to input
61
+ const cloned = { ...body, input: [...input] };
62
+ cloned.input.push({
63
+ type: "message",
64
+ role: "user",
65
+ content: [{ type: "input_text", text: LOOP_WARNING_PROMPT }],
66
+ });
67
+ return { body: cloned, injected: true, rounds: funcCalls };
68
+ }
53
69
  const messages = body.messages ?? [];
54
70
  if (messages.length === 0)
55
71
  return { body, injected: false, rounds: 0 };
@@ -30,5 +30,5 @@ export declare function createErrorFormatter(formatBody: (kind: ErrorKind, messa
30
30
  export declare function buildUpstreamUrl(baseUrl: string, upstreamPath: string): string;
31
31
  export declare const SKIP_UPSTREAM: Set<string>;
32
32
  export declare function selectHeaders(raw: RawHeaders, skip: Set<string>): Record<string, string>;
33
- export declare function buildUpstreamHeaders(clientHeaders: RawHeaders, apiKey: string, payloadBytes?: number, apiType?: "openai" | "anthropic"): Record<string, string>;
33
+ export declare function buildUpstreamHeaders(clientHeaders: RawHeaders, apiKey: string, payloadBytes?: number, apiType?: "openai" | "openai-responses" | "anthropic"): Record<string, string>;
34
34
  export declare function proxyGetRequest(backend: Provider, apiKey: string, clientHeaders: RawHeaders, upstreamPath: string): Promise<GetTransportResult>;
@@ -6,7 +6,7 @@ import type { ResilienceAttempt } from "../core/types.js";
6
6
  import type { TransportResult } from "./types.js";
7
7
  /** 日志存储前脱敏 Authorization / x-api-key header,避免 API Key 被持久化 */
8
8
  export declare function sanitizeHeadersForLog(headers: Record<string, string>): Record<string, string>;
9
- export declare function handleIntercept(db: Database.Database, apiType: "openai" | "anthropic", request: FastifyRequest, reply: import("fastify").FastifyReply, interceptResponse: {
9
+ export declare function handleIntercept(db: Database.Database, apiType: "openai" | "openai-responses" | "anthropic", request: FastifyRequest, reply: import("fastify").FastifyReply, interceptResponse: {
10
10
  statusCode: number;
11
11
  body: unknown;
12
12
  meta?: unknown;
@@ -14,7 +14,7 @@ export declare function handleIntercept(db: Database.Database, apiType: "openai"
14
14
  test: (statusCode: number, body: string) => boolean;
15
15
  } | null, logFileWriter?: LogFileWriter | null): import("fastify").FastifyReply;
16
16
  export declare function logResilienceResult(db: Database.Database, params: {
17
- apiType: "openai" | "anthropic";
17
+ apiType: "openai" | "openai-responses" | "anthropic";
18
18
  model: string;
19
19
  providerId: string;
20
20
  isStream: boolean;
@@ -31,4 +31,4 @@ export declare function logResilienceResult(db: Database.Database, params: {
31
31
  } | null;
32
32
  logFileWriter?: LogFileWriter | null;
33
33
  }, attempts: ResilienceAttempt[], result: TransportResult, startTime: number): string;
34
- export declare function collectTransportMetrics(db: Database.Database, apiType: "openai" | "anthropic", result: TransportResult, isStream: boolean, lastSuccessLogId: string, providerId: string, backendModel: string, request: FastifyRequest, routerKeyId?: string | null, statusCode?: number | null): void;
34
+ export declare function collectTransportMetrics(db: Database.Database, apiType: "openai" | "openai-responses" | "anthropic", result: TransportResult, isStream: boolean, lastSuccessLogId: string, providerId: string, backendModel: string, request: FastifyRequest, routerKeyId?: string | null, statusCode?: number | null): void;
@@ -9,6 +9,19 @@ export function maybeInjectModelInfoTag(responseBody, originalModel, effectiveMo
9
9
  bodyObj.content[0].text += `\n\n${buildModelInfoTag(effectiveModel)}`;
10
10
  return { body: JSON.stringify(bodyObj), meta: { model_info_tag_injected: true } };
11
11
  }
12
+ // Responses format: output[type=message].content[type=output_text].text
13
+ if (Array.isArray(bodyObj.output)) {
14
+ for (const item of bodyObj.output) {
15
+ if (item.type === "message" && Array.isArray(item.content)) {
16
+ for (const part of item.content) {
17
+ if (part.type === "output_text" && part.text) {
18
+ part.text = part.text + `\n\n${buildModelInfoTag(effectiveModel)}`;
19
+ return { body: JSON.stringify(bodyObj), meta: { model_info_tag_injected: true } };
20
+ }
21
+ }
22
+ }
23
+ }
24
+ }
12
25
  }
13
26
  catch { /* non-JSON response, skip injection */ }
14
27
  return { body: responseBody, meta: { model_info_tag_injected: false } };
@@ -1,3 +1,4 @@
1
1
  export declare function generateMsgId(): string;
2
2
  export declare function generateChatcmplId(): string;
3
+ export declare function generateRespId(): string;
3
4
  export declare const MS_PER_SECOND = 1000;
@@ -6,4 +6,7 @@ export function generateMsgId() {
6
6
  export function generateChatcmplId() {
7
7
  return `chatcmpl-${randomUUID().slice(0, UUID_ID_LENGTH)}`;
8
8
  }
9
+ export function generateRespId() {
10
+ return `resp_${randomUUID().slice(0, UUID_ID_LENGTH)}`;
11
+ }
9
12
  export const MS_PER_SECOND = 1000;
@@ -5,13 +5,13 @@ export interface PluginMatch {
5
5
  providerId?: string;
6
6
  providerName?: string;
7
7
  providerNamePattern?: string;
8
- apiType?: "openai" | "anthropic";
8
+ apiType?: "openai" | "openai-responses" | "anthropic";
9
9
  }
10
10
  export interface RequestTransformContext {
11
11
  body: Record<string, unknown>;
12
12
  headers: Record<string, string>;
13
- sourceApiType: "openai" | "anthropic";
14
- targetApiType: "openai" | "anthropic";
13
+ sourceApiType: "openai" | "openai-responses" | "anthropic";
14
+ targetApiType: "openai" | "openai-responses" | "anthropic";
15
15
  provider: {
16
16
  id: string;
17
17
  name: string;
@@ -21,8 +21,8 @@ export interface RequestTransformContext {
21
21
  }
22
22
  export interface ResponseTransformContext {
23
23
  response: Record<string, unknown>;
24
- sourceApiType: "openai" | "anthropic";
25
- targetApiType: "openai" | "anthropic";
24
+ sourceApiType: "openai" | "openai-responses" | "anthropic";
25
+ targetApiType: "openai" | "openai-responses" | "anthropic";
26
26
  provider: {
27
27
  id: string;
28
28
  name: string;
@@ -0,0 +1,19 @@
1
+ /**
2
+ * Bridge (lossy) request transformation between OpenAI Responses API
3
+ * and OpenAI Chat Completions API.
4
+ *
5
+ * This is the SECONDARY conversion path used when the upstream provider
6
+ * only supports the opposite API format. It is lossy because Chat Completions
7
+ * cannot represent `previous_response_id`, built-in tools, or structured
8
+ * reasoning items.
9
+ */
10
+ /**
11
+ * Convert an OpenAI Responses API request body to an OpenAI Chat Completions
12
+ * request body.
13
+ */
14
+ export declare function responsesToChatRequest(body: Record<string, unknown>): Record<string, unknown>;
15
+ /**
16
+ * Convert an OpenAI Chat Completions request body to an OpenAI Responses API
17
+ * request body.
18
+ */
19
+ export declare function chatToResponsesRequest(body: Record<string, unknown>): Record<string, unknown>;