waypoi 0.0.0

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 (260) hide show
  1. package/.github/instructions/ui.instructions.md +42 -0
  2. package/.github/workflows/ci.yml +35 -0
  3. package/.github/workflows/publish.yml +71 -0
  4. package/.github/workflows/release.yml +48 -0
  5. package/.playwright-mcp/console-2026-04-04T01-41-10-746Z.log +2 -0
  6. package/.playwright-mcp/console-2026-04-04T01-41-28-799Z.log +3 -0
  7. package/.playwright-mcp/console-2026-04-05T02-26-51-909Z.log +76 -0
  8. package/.playwright-mcp/page-2026-04-04T01-41-10-816Z.yml +1 -0
  9. package/.playwright-mcp/page-2026-04-04T01-41-29-141Z.yml +77 -0
  10. package/.playwright-mcp/page-2026-04-04T01-41-42-633Z.yml +190 -0
  11. package/.playwright-mcp/page-2026-04-04T01-42-03-929Z.yml +262 -0
  12. package/.playwright-mcp/page-2026-04-04T02-12-54-813Z.yml +6 -0
  13. package/.playwright-mcp/page-2026-04-04T02-14-58-600Z.yml +190 -0
  14. package/.playwright-mcp/page-2026-04-04T02-15-03-923Z.yml +190 -0
  15. package/.playwright-mcp/page-2026-04-04T02-15-07-426Z.yml +190 -0
  16. package/.playwright-mcp/page-2026-04-04T02-15-25-729Z.yml +262 -0
  17. package/.playwright-mcp/page-2026-04-04T02-16-22-984Z.yml +262 -0
  18. package/.playwright-mcp/page-2026-04-04T02-17-00-599Z.yml +190 -0
  19. package/.playwright-mcp/page-2026-04-04T02-17-50-874Z.yml +190 -0
  20. package/.playwright-mcp/page-2026-04-05T02-26-55-570Z.yml +6 -0
  21. package/AGENTS.md +48 -0
  22. package/CHANGELOG.md +131 -0
  23. package/README.md +552 -0
  24. package/assets/agent-mode.png +0 -0
  25. package/assets/categorize.png +0 -0
  26. package/assets/dashboard.png +0 -0
  27. package/assets/endpoint-proxy.png +0 -0
  28. package/assets/icon.png +0 -0
  29. package/assets/mcp-generate-image.png +0 -0
  30. package/assets/mcp-understand-image.png +0 -0
  31. package/assets/peek-token-flow.png +0 -0
  32. package/assets/playground.png +0 -0
  33. package/assets/sankey.png +0 -0
  34. package/cli/index.ts +2805 -0
  35. package/cli/legacyRewrite.ts +108 -0
  36. package/cli/modelRef.ts +24 -0
  37. package/dist/cli/index.js +2536 -0
  38. package/dist/cli/legacyRewrite.js +92 -0
  39. package/dist/cli/modelRef.js +20 -0
  40. package/dist/src/benchmark/artifacts.js +131 -0
  41. package/dist/src/benchmark/capabilityClassifier.js +81 -0
  42. package/dist/src/benchmark/capabilityStore.js +144 -0
  43. package/dist/src/benchmark/config.js +238 -0
  44. package/dist/src/benchmark/gates.js +118 -0
  45. package/dist/src/benchmark/jobs.js +252 -0
  46. package/dist/src/benchmark/runner.js +1847 -0
  47. package/dist/src/benchmark/schema.js +353 -0
  48. package/dist/src/benchmark/suites.js +314 -0
  49. package/dist/src/benchmark/tinyQaDataset.js +422 -0
  50. package/dist/src/benchmark/types.js +25 -0
  51. package/dist/src/config.js +47 -0
  52. package/dist/src/index.js +178 -0
  53. package/dist/src/mcp/client.js +215 -0
  54. package/dist/src/mcp/discovery.js +226 -0
  55. package/dist/src/mcp/policy.js +65 -0
  56. package/dist/src/mcp/registry.js +129 -0
  57. package/dist/src/mcp/service.js +460 -0
  58. package/dist/src/middleware/auth.js +179 -0
  59. package/dist/src/middleware/requestCapture.js +192 -0
  60. package/dist/src/middleware/requestStats.js +118 -0
  61. package/dist/src/pools/builder.js +132 -0
  62. package/dist/src/pools/repository.js +69 -0
  63. package/dist/src/pools/scheduler.js +360 -0
  64. package/dist/src/pools/types.js +2 -0
  65. package/dist/src/protocols/adapters/dashscope.js +267 -0
  66. package/dist/src/protocols/adapters/inferenceV2.js +346 -0
  67. package/dist/src/protocols/adapters/openai.js +27 -0
  68. package/dist/src/protocols/registry.js +99 -0
  69. package/dist/src/protocols/types.js +2 -0
  70. package/dist/src/providers/health.js +153 -0
  71. package/dist/src/providers/importer.js +289 -0
  72. package/dist/src/providers/modelRegistry.js +313 -0
  73. package/dist/src/providers/repository.js +361 -0
  74. package/dist/src/providers/types.js +2 -0
  75. package/dist/src/routes/admin.js +531 -0
  76. package/dist/src/routes/audio.js +295 -0
  77. package/dist/src/routes/chat.js +240 -0
  78. package/dist/src/routes/embeddings.js +157 -0
  79. package/dist/src/routes/images.js +288 -0
  80. package/dist/src/routes/mcp.js +256 -0
  81. package/dist/src/routes/mcpService.js +100 -0
  82. package/dist/src/routes/models.js +48 -0
  83. package/dist/src/routes/responses.js +711 -0
  84. package/dist/src/routes/sessions.js +450 -0
  85. package/dist/src/routes/stats.js +270 -0
  86. package/dist/src/routes/ui.js +97 -0
  87. package/dist/src/routes/videos.js +107 -0
  88. package/dist/src/routing/router.js +338 -0
  89. package/dist/src/services/imageGeneration.js +280 -0
  90. package/dist/src/services/imageUnderstanding.js +352 -0
  91. package/dist/src/services/videoGeneration.js +79 -0
  92. package/dist/src/storage/captureRepository.js +1591 -0
  93. package/dist/src/storage/files.js +157 -0
  94. package/dist/src/storage/imageCache.js +346 -0
  95. package/dist/src/storage/repositories.js +388 -0
  96. package/dist/src/storage/sessionRepository.js +370 -0
  97. package/dist/src/storage/statsRepository.js +204 -0
  98. package/dist/src/transport/httpClient.js +126 -0
  99. package/dist/src/types.js +2 -0
  100. package/dist/src/utils/messageMedia.js +285 -0
  101. package/dist/src/utils/modelCapabilities.js +108 -0
  102. package/dist/src/utils/modelDiscovery.js +170 -0
  103. package/dist/src/version.js +5 -0
  104. package/dist/src/workers/captureRetention.js +25 -0
  105. package/dist/src/workers/configWatcher.js +91 -0
  106. package/dist/src/workers/healthChecker.js +21 -0
  107. package/dist/src/workers/statsRotation.js +41 -0
  108. package/docs/LLM/output_schema.md +312 -0
  109. package/docs/benchmark.md +208 -0
  110. package/docs/mcp-guidelines.md +125 -0
  111. package/docs/mcp-service.md +178 -0
  112. package/docs/opencode.md +86 -0
  113. package/docs/providers.md +79 -0
  114. package/examples/benchmark.config.yaml +28 -0
  115. package/examples/providers/alibaba-dashscope.yaml +88 -0
  116. package/examples/providers/alibaba-llm.yaml +64 -0
  117. package/examples/providers/alibaba-registry.yaml +7 -0
  118. package/examples/providers/inference-v2-ray.yaml +29 -0
  119. package/examples/scenarios/assets/omni-call-sample.wav +0 -0
  120. package/examples/scenarios/custom.jsonl +5 -0
  121. package/examples/scenarios/custom.yaml +40 -0
  122. package/model-form-v2.png +0 -0
  123. package/package.json +66 -0
  124. package/provider-form-v2.png +0 -0
  125. package/provider-form.png +0 -0
  126. package/scripts/manual-test.sh +11 -0
  127. package/scripts/version-from-git.js +23 -0
  128. package/src/benchmark/artifacts.ts +149 -0
  129. package/src/benchmark/capabilityClassifier.ts +99 -0
  130. package/src/benchmark/capabilityStore.ts +174 -0
  131. package/src/benchmark/config.ts +337 -0
  132. package/src/benchmark/gates.ts +164 -0
  133. package/src/benchmark/jobs.ts +312 -0
  134. package/src/benchmark/runner.ts +2519 -0
  135. package/src/benchmark/schema.ts +443 -0
  136. package/src/benchmark/suites.ts +323 -0
  137. package/src/benchmark/tinyQaDataset.ts +428 -0
  138. package/src/benchmark/types.ts +442 -0
  139. package/src/config.ts +44 -0
  140. package/src/index.ts +195 -0
  141. package/src/mcp/client.ts +305 -0
  142. package/src/mcp/discovery.ts +266 -0
  143. package/src/mcp/policy.ts +105 -0
  144. package/src/mcp/registry.ts +164 -0
  145. package/src/mcp/service.ts +611 -0
  146. package/src/middleware/auth.ts +251 -0
  147. package/src/middleware/requestCapture.ts +245 -0
  148. package/src/middleware/requestStats.ts +163 -0
  149. package/src/pools/builder.ts +159 -0
  150. package/src/pools/repository.ts +71 -0
  151. package/src/pools/scheduler.ts +425 -0
  152. package/src/pools/types.ts +117 -0
  153. package/src/protocols/adapters/dashscope.ts +335 -0
  154. package/src/protocols/adapters/inferenceV2.ts +428 -0
  155. package/src/protocols/adapters/openai.ts +32 -0
  156. package/src/protocols/registry.ts +117 -0
  157. package/src/protocols/types.ts +81 -0
  158. package/src/providers/health.ts +207 -0
  159. package/src/providers/importer.ts +402 -0
  160. package/src/providers/modelRegistry.ts +415 -0
  161. package/src/providers/repository.ts +439 -0
  162. package/src/providers/types.ts +113 -0
  163. package/src/routes/admin.ts +666 -0
  164. package/src/routes/audio.ts +372 -0
  165. package/src/routes/chat.ts +301 -0
  166. package/src/routes/embeddings.ts +197 -0
  167. package/src/routes/images.ts +356 -0
  168. package/src/routes/mcp.ts +320 -0
  169. package/src/routes/mcpService.ts +114 -0
  170. package/src/routes/models.ts +50 -0
  171. package/src/routes/responses.ts +872 -0
  172. package/src/routes/sessions.ts +558 -0
  173. package/src/routes/stats.ts +312 -0
  174. package/src/routes/ui.ts +96 -0
  175. package/src/routes/videos.ts +132 -0
  176. package/src/routing/router.ts +501 -0
  177. package/src/services/imageGeneration.ts +396 -0
  178. package/src/services/imageUnderstanding.ts +449 -0
  179. package/src/services/videoGeneration.ts +127 -0
  180. package/src/storage/captureRepository.ts +1835 -0
  181. package/src/storage/files.ts +178 -0
  182. package/src/storage/imageCache.ts +405 -0
  183. package/src/storage/repositories.ts +494 -0
  184. package/src/storage/sessionRepository.ts +419 -0
  185. package/src/storage/statsRepository.ts +238 -0
  186. package/src/transport/httpClient.ts +145 -0
  187. package/src/types.ts +322 -0
  188. package/src/utils/messageMedia.ts +293 -0
  189. package/src/utils/modelCapabilities.ts +161 -0
  190. package/src/utils/modelDiscovery.ts +203 -0
  191. package/src/workers/captureRetention.ts +25 -0
  192. package/src/workers/configWatcher.ts +115 -0
  193. package/src/workers/healthChecker.ts +22 -0
  194. package/src/workers/statsRotation.ts +49 -0
  195. package/tests/benchmarkAdminRoutes.test.ts +82 -0
  196. package/tests/benchmarkBasics.test.ts +116 -0
  197. package/tests/captureAdminRoutes.test.ts +420 -0
  198. package/tests/captureRepository.test.ts +797 -0
  199. package/tests/cliLegacyRewrite.test.ts +45 -0
  200. package/tests/imageGeneration.service.test.ts +107 -0
  201. package/tests/imageUnderstanding.service.test.ts +123 -0
  202. package/tests/mcpPolicy.test.ts +105 -0
  203. package/tests/mcpService.test.ts +1245 -0
  204. package/tests/modelRef.test.ts +23 -0
  205. package/tests/modelsRoutes.test.ts +154 -0
  206. package/tests/sessionMediaCache.test.ts +167 -0
  207. package/tests/statsRoutes.test.ts +323 -0
  208. package/tsconfig.json +15 -0
  209. package/ui/index.html +16 -0
  210. package/ui/package-lock.json +8521 -0
  211. package/ui/package.json +52 -0
  212. package/ui/postcss.config.js +6 -0
  213. package/ui/public/assets/apple-touch-icon.png +0 -0
  214. package/ui/public/assets/favicon-16.png +0 -0
  215. package/ui/public/assets/favicon-32.png +0 -0
  216. package/ui/public/assets/icon-192.png +0 -0
  217. package/ui/public/assets/icon-512.png +0 -0
  218. package/ui/src/App.tsx +27 -0
  219. package/ui/src/api/client.ts +1503 -0
  220. package/ui/src/components/EndpointUsageGuide.tsx +361 -0
  221. package/ui/src/components/Layout.tsx +124 -0
  222. package/ui/src/components/MessageContent.tsx +365 -0
  223. package/ui/src/components/ToolCallMessage.tsx +179 -0
  224. package/ui/src/components/ToolPicker.tsx +442 -0
  225. package/ui/src/components/messageContentParser.test.ts +41 -0
  226. package/ui/src/components/messageContentParser.ts +73 -0
  227. package/ui/src/components/thinkingPreview.test.ts +27 -0
  228. package/ui/src/components/thinkingPreview.ts +15 -0
  229. package/ui/src/components/toMermaidSankey.test.ts +78 -0
  230. package/ui/src/components/toMermaidSankey.ts +56 -0
  231. package/ui/src/components/ui/button.tsx +58 -0
  232. package/ui/src/components/ui/input.tsx +21 -0
  233. package/ui/src/components/ui/textarea.tsx +21 -0
  234. package/ui/src/lib/utils.ts +6 -0
  235. package/ui/src/main.tsx +9 -0
  236. package/ui/src/pages/AgentPlayground.tsx +2010 -0
  237. package/ui/src/pages/Benchmark.tsx +988 -0
  238. package/ui/src/pages/Dashboard.tsx +581 -0
  239. package/ui/src/pages/Peek.tsx +962 -0
  240. package/ui/src/pages/Settings.tsx +2013 -0
  241. package/ui/src/pages/agentPlaygroundPayload.test.ts +109 -0
  242. package/ui/src/pages/agentPlaygroundPayload.ts +97 -0
  243. package/ui/src/pages/agentThinkingContent.test.ts +50 -0
  244. package/ui/src/pages/agentThinkingContent.ts +57 -0
  245. package/ui/src/pages/dashboardTokenUsage.test.ts +66 -0
  246. package/ui/src/pages/dashboardTokenUsage.ts +36 -0
  247. package/ui/src/pages/imageUpload.test.ts +39 -0
  248. package/ui/src/pages/imageUpload.ts +71 -0
  249. package/ui/src/pages/peekFilters.test.ts +29 -0
  250. package/ui/src/pages/peekFilters.ts +13 -0
  251. package/ui/src/pages/peekMedia.test.ts +58 -0
  252. package/ui/src/pages/peekMedia.ts +148 -0
  253. package/ui/src/pages/sessionAutoTitle.test.ts +128 -0
  254. package/ui/src/pages/sessionAutoTitle.ts +106 -0
  255. package/ui/src/stores/settings.ts +58 -0
  256. package/ui/src/styles/globals.css +223 -0
  257. package/ui/src/vite-env.d.ts +8 -0
  258. package/ui/tailwind.config.js +106 -0
  259. package/ui/tsconfig.json +32 -0
  260. package/ui/vite.config.ts +37 -0
@@ -0,0 +1,711 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.registerResponsesRoutes = registerResponsesRoutes;
4
+ const crypto_1 = require("crypto");
5
+ const stream_1 = require("stream");
6
+ const router_1 = require("../routing/router");
7
+ const repositories_1 = require("../storage/repositories");
8
+ const scheduler_1 = require("../pools/scheduler");
9
+ const modelRegistry_1 = require("../providers/modelRegistry");
10
+ const messageMedia_1 = require("../utils/messageMedia");
11
+ const requestCapture_1 = require("../middleware/requestCapture");
12
+ const requestStats_1 = require("../middleware/requestStats");
13
+ /**
14
+ * Responses API compatibility shim.
15
+ *
16
+ * Some newer SDK flows prefer the "Responses API" pattern. This endpoint
17
+ * translates those requests to /v1/chat/completions internally.
18
+ *
19
+ * Input formats supported:
20
+ * - { input: "string" } → single user message
21
+ * - { input: [{ role, content }] } → message array
22
+ * - { instructions: "..." } → system message prepended
23
+ */
24
+ async function registerResponsesRoutes(app, paths) {
25
+ app.post("/v1/responses", async (req, reply) => {
26
+ const body = req.body;
27
+ if (!body?.model) {
28
+ const fallback = await pickDefaultModel(paths);
29
+ if (!fallback) {
30
+ reply.code(400).send({ error: { message: "model is required" } });
31
+ return;
32
+ }
33
+ if (body)
34
+ body.model = fallback;
35
+ }
36
+ if (!body?.input) {
37
+ reply.code(400).send({ error: { message: "input is required" } });
38
+ return;
39
+ }
40
+ // Transform to chat completions format
41
+ const messages = transformToMessages(body);
42
+ const transformedTools = body.tools ? transformTools(body.tools) : undefined;
43
+ // Track if client wants streaming
44
+ const clientWantsStreaming = body.stream ?? false;
45
+ const normalizedMessages = await (0, messageMedia_1.normalizeMessagesForUpstream)(paths, messages);
46
+ const media = (0, messageMedia_1.scanMessageModalities)(normalizedMessages);
47
+ const chatPayload = {
48
+ model: body.model,
49
+ messages: normalizedMessages,
50
+ stream: clientWantsStreaming, // Pass through streaming preference
51
+ temperature: body.temperature,
52
+ top_p: body.top_p,
53
+ max_tokens: body.max_tokens,
54
+ presence_penalty: body.presence_penalty,
55
+ frequency_penalty: body.frequency_penalty,
56
+ seed: body.seed,
57
+ stop: body.stop,
58
+ tools: transformedTools,
59
+ tool_choice: body.tool_choice
60
+ };
61
+ (0, requestCapture_1.setCaptureDerivedRequest)(reply, {
62
+ originalRequest: body,
63
+ normalizedRequest: chatPayload,
64
+ });
65
+ const requestId = (0, crypto_1.randomUUID)();
66
+ const start = Date.now();
67
+ const controller = new AbortController();
68
+ req.raw.on("close", () => controller.abort());
69
+ try {
70
+ const outcome = await (0, router_1.routeRequest)(paths, body.model, "/v1/chat/completions", chatPayload, req.headers, controller.signal, {
71
+ requiredInput: media.hasAudio
72
+ ? media.hasImage
73
+ ? ["text", "image", "audio"]
74
+ : ["text", "audio"]
75
+ : media.hasImage
76
+ ? ["text", "image"]
77
+ : ["text"],
78
+ requiredOutput: ["text"],
79
+ });
80
+ // Handle streaming response
81
+ if (clientWantsStreaming) {
82
+ await streamResponsesAPI(reply, outcome.attempt.response, requestId, body.model);
83
+ (0, requestCapture_1.setCaptureResponseOverride)(reply, {
84
+ $type: "stream",
85
+ contentType: "text/event-stream",
86
+ note: "Responses API SSE stream captured as metadata",
87
+ }, outcome.attempt.response.headers);
88
+ (0, requestCapture_1.setCaptureRouting)(reply, {
89
+ publicModel: body.model,
90
+ endpointId: outcome.attempt.endpoint.id,
91
+ endpointName: outcome.attempt.endpoint.name,
92
+ upstreamModel: outcome.attempt.upstreamModel,
93
+ });
94
+ (0, requestStats_1.setStatsPayload)(reply, {
95
+ endpointId: outcome.attempt.endpoint.id,
96
+ endpointName: outcome.attempt.endpoint.name,
97
+ upstreamModel: outcome.attempt.upstreamModel,
98
+ });
99
+ await (0, repositories_1.logRequest)(paths, buildLog(requestId, body.model, outcome, Date.now() - start, true, 0 // Token count not available in streaming
100
+ ));
101
+ return;
102
+ }
103
+ // Non-streaming response
104
+ const upstreamBody = await readBody(outcome.attempt.response);
105
+ // Transform response to Responses API format
106
+ const responsesFormat = transformToResponsesFormat(upstreamBody.payload, requestId);
107
+ setHeaders(reply, outcome.attempt.response.headers);
108
+ reply.code(outcome.attempt.response.statusCode).send(responsesFormat);
109
+ (0, requestCapture_1.setCaptureRouting)(reply, {
110
+ publicModel: body.model,
111
+ endpointId: outcome.attempt.endpoint.id,
112
+ endpointName: outcome.attempt.endpoint.name,
113
+ upstreamModel: outcome.attempt.upstreamModel,
114
+ });
115
+ (0, requestStats_1.setStatsPayload)(reply, {
116
+ endpointId: outcome.attempt.endpoint.id,
117
+ endpointName: outcome.attempt.endpoint.name,
118
+ upstreamModel: outcome.attempt.upstreamModel,
119
+ totalTokens: upstreamBody.totalTokens,
120
+ promptTokens: upstreamBody.promptTokens,
121
+ completionTokens: upstreamBody.completionTokens,
122
+ });
123
+ await (0, repositories_1.logRequest)(paths, buildLog(requestId, body.model, outcome, Date.now() - start, false, upstreamBody.totalTokens));
124
+ }
125
+ catch (error) {
126
+ const errorType = error.type ?? error.name;
127
+ (0, requestCapture_1.setCaptureError)(reply, { type: errorType, message: error.message });
128
+ await (0, repositories_1.logRequest)(paths, {
129
+ requestId,
130
+ ts: new Date(),
131
+ route: { publicModel: body?.model ?? "unknown" },
132
+ request: { stream: Boolean(body?.stream) },
133
+ result: { errorType, errorMessage: error.message }
134
+ });
135
+ // Don't try to send error if headers already sent (streaming started)
136
+ if (reply.raw.headersSent) {
137
+ req.log.warn({ err: error }, "Error after streaming started");
138
+ reply.raw.end();
139
+ return;
140
+ }
141
+ if (errorType === "invalid_request") {
142
+ reply.code(400).send({ error: { message: error.message } });
143
+ return;
144
+ }
145
+ if (errorType === "tls_verify_failed") {
146
+ reply.code(502).send({ error: { message: error.message } });
147
+ return;
148
+ }
149
+ const status = errorType === "no_endpoints" ||
150
+ errorType === "protocol_stream_unsupported" ||
151
+ errorType === "unsupported_protocol" ||
152
+ errorType === "invalid_protocol_config"
153
+ ? 400
154
+ : errorType === "rate_limited"
155
+ ? 429
156
+ : 502;
157
+ reply.code(status).send({ error: { message: "Upstream unavailable" } });
158
+ }
159
+ });
160
+ }
161
+ /**
162
+ * Transform Responses-style input to OpenAI chat completions messages.
163
+ *
164
+ * Some clients send a variety of item types:
165
+ * - { type: "message", role: "user/assistant/developer", content: [...] }
166
+ * - { type: "function_call", name: "...", arguments: "...", call_id: "..." }
167
+ * - { type: "function_call_output", call_id: "...", output: "..." }
168
+ *
169
+ * OpenAI chat completions expects:
170
+ * - { role: "user/assistant/system", content: "..." }
171
+ * - Assistant messages can have tool_calls: [{ id, type: "function", function: { name, arguments } }]
172
+ * - { role: "tool", tool_call_id: "...", content: "..." }
173
+ */
174
+ function transformToMessages(body) {
175
+ const messages = [];
176
+ // Add system message from instructions if present
177
+ if (body.instructions) {
178
+ messages.push({ role: "system", content: body.instructions });
179
+ }
180
+ // Transform input
181
+ if (typeof body.input === "string") {
182
+ messages.push({ role: "user", content: body.input });
183
+ }
184
+ else if (Array.isArray(body.input)) {
185
+ // Process items, grouping consecutive function_calls into a single assistant message
186
+ let pendingToolCalls = [];
187
+ for (const item of body.input) {
188
+ if (!item || typeof item !== "object")
189
+ continue;
190
+ const itemObj = item;
191
+ const itemType = itemObj.type;
192
+ // Handle function_call items - need to be grouped into an assistant message
193
+ if (itemType === "function_call") {
194
+ pendingToolCalls.push({
195
+ id: itemObj.call_id || itemObj.id || "",
196
+ type: "function",
197
+ function: {
198
+ name: itemObj.name,
199
+ arguments: itemObj.arguments
200
+ }
201
+ });
202
+ continue;
203
+ }
204
+ // Before processing other items, flush any pending tool calls
205
+ if (pendingToolCalls.length > 0) {
206
+ messages.push({
207
+ role: "assistant",
208
+ content: null,
209
+ tool_calls: pendingToolCalls
210
+ });
211
+ pendingToolCalls = [];
212
+ }
213
+ // Handle function_call_output items - become tool role messages
214
+ if (itemType === "function_call_output") {
215
+ messages.push({
216
+ role: "tool",
217
+ tool_call_id: itemObj.call_id,
218
+ content: typeof itemObj.output === "string" ? itemObj.output : JSON.stringify(itemObj.output)
219
+ });
220
+ continue;
221
+ }
222
+ // Handle regular message items
223
+ if (itemType === "message" && "role" in itemObj && "content" in itemObj) {
224
+ const role = itemObj.role;
225
+ // Map developer role to system
226
+ const mappedRole = role === "developer" ? "system" : role;
227
+ const content = transformMessageContent(itemObj.content);
228
+ messages.push({ role: mappedRole, content });
229
+ continue;
230
+ }
231
+ // Handle items with role/content directly (legacy format)
232
+ if ("role" in itemObj && "content" in itemObj) {
233
+ const role = itemObj.role;
234
+ const mappedRole = role === "developer" ? "system" : role;
235
+ const content = transformMessageContent(itemObj.content);
236
+ messages.push({ role: mappedRole, content });
237
+ continue;
238
+ }
239
+ }
240
+ // Flush any remaining pending tool calls
241
+ if (pendingToolCalls.length > 0) {
242
+ messages.push({
243
+ role: "assistant",
244
+ content: null,
245
+ tool_calls: pendingToolCalls
246
+ });
247
+ }
248
+ }
249
+ return messages;
250
+ }
251
+ /**
252
+ * Transform message content, normalizing response content part types to OpenAI format.
253
+ * Some clients send: { type: "input_text", text: "..." } for user messages
254
+ * Some clients send: { type: "output_text", text: "..." } for assistant messages
255
+ * OpenAI expects: { type: "text", text: "..." }
256
+ */
257
+ function transformMessageContent(content) {
258
+ if (typeof content === "string") {
259
+ return content;
260
+ }
261
+ if (Array.isArray(content)) {
262
+ return content.map(part => {
263
+ if (part && typeof part === "object") {
264
+ const p = part;
265
+ // Normalize input_text/output_text to OpenAI text
266
+ // input_text is typically user content, output_text assistant content
267
+ if (p.type === "input_text" || p.type === "output_text") {
268
+ return { ...p, type: "text" };
269
+ }
270
+ if (p.type === "input_image" && p.image_url) {
271
+ return { ...p, type: "image_url" };
272
+ }
273
+ // Accept shorthand {type:\"audio\", audio:\"...\"} and normalize downstream
274
+ if (p.type === "input_audio" || p.type === "audio" || p.type === "video") {
275
+ return p;
276
+ }
277
+ }
278
+ return part;
279
+ });
280
+ }
281
+ // Fallback: return as array containing the original content
282
+ return [content];
283
+ }
284
+ /**
285
+ * Transform Responses-style tools to OpenAI function-calling format.
286
+ *
287
+ * Some clients send tools like:
288
+ * { type: "function", name: "...", description: "...", parameters: {...} }
289
+ *
290
+ * OpenAI expects:
291
+ * { type: "function", function: { name: "...", description: "...", parameters: {...} } }
292
+ *
293
+ * Special case: web_search tools are filtered out as they're not supported by OpenAI format.
294
+ */
295
+ function transformTools(tools) {
296
+ return tools
297
+ .filter(tool => {
298
+ // Filter out web_search tools - not supported in OpenAI function calling format
299
+ if (tool && typeof tool === "object") {
300
+ const t = tool;
301
+ if (t.type === "web_search") {
302
+ return false;
303
+ }
304
+ }
305
+ return true;
306
+ })
307
+ .map(tool => {
308
+ if (!tool || typeof tool !== "object")
309
+ return tool;
310
+ const t = tool;
311
+ // If already in OpenAI format (has 'function' property), return as-is
312
+ if (t.function)
313
+ return tool;
314
+ // If has type="function" but no 'function' wrapper, wrap it
315
+ if (t.type === "function") {
316
+ const { type, ...functionDef } = t;
317
+ return {
318
+ type,
319
+ function: functionDef
320
+ };
321
+ }
322
+ // Otherwise return unchanged
323
+ return tool;
324
+ });
325
+ }
326
+ function transformToResponsesFormat(chatResponse, requestId) {
327
+ if (!chatResponse || typeof chatResponse !== "object") {
328
+ return {
329
+ id: requestId,
330
+ object: "response",
331
+ created_at: Math.floor(Date.now() / 1000),
332
+ output: []
333
+ };
334
+ }
335
+ const chat = chatResponse;
336
+ const firstChoice = chat.choices?.[0];
337
+ const message = firstChoice?.message;
338
+ const output = [];
339
+ // Handle tool calls if present
340
+ if (message?.tool_calls && message.tool_calls.length > 0) {
341
+ for (const toolCall of message.tool_calls) {
342
+ output.push({
343
+ type: "function_call",
344
+ id: toolCall.id,
345
+ call_id: toolCall.id,
346
+ name: toolCall.function.name,
347
+ arguments: toolCall.function.arguments
348
+ });
349
+ }
350
+ }
351
+ // Handle text content
352
+ const textContent = message?.content ?? "";
353
+ if (textContent || output.length === 0) {
354
+ output.push({
355
+ type: "message",
356
+ role: message?.role ?? "assistant",
357
+ // Responses-style clients may expect output_text instead of text
358
+ content: [{ type: "output_text", text: textContent }]
359
+ });
360
+ }
361
+ return {
362
+ id: chat.id ?? requestId,
363
+ object: "response",
364
+ created_at: chat.created ?? Math.floor(Date.now() / 1000),
365
+ model: chat.model,
366
+ output,
367
+ usage: chat.usage ? {
368
+ input_tokens: chat.usage.prompt_tokens ?? 0,
369
+ output_tokens: chat.usage.completion_tokens ?? 0,
370
+ total_tokens: chat.usage.total_tokens ?? 0
371
+ } : undefined
372
+ };
373
+ }
374
+ /**
375
+ * Stream chat completions response and transform to Responses API SSE format.
376
+ *
377
+ * This reads the upstream SSE stream (chat.completion.chunk format) and
378
+ * transforms it to Responses API format in real-time.
379
+ */
380
+ async function streamResponsesAPI(reply, upstreamResponse, requestId, model) {
381
+ reply.raw.writeHead(200, {
382
+ "Content-Type": "text/event-stream",
383
+ "Cache-Control": "no-cache",
384
+ "Connection": "keep-alive"
385
+ });
386
+ const sendEvent = (eventType, data) => {
387
+ reply.raw.write(`event: ${eventType}\n`);
388
+ reply.raw.write(`data: ${JSON.stringify(data)}\n\n`);
389
+ };
390
+ // Send response.created immediately
391
+ sendEvent("response.created", {
392
+ type: "response.created",
393
+ response: {
394
+ id: requestId,
395
+ object: "response",
396
+ created_at: Math.floor(Date.now() / 1000),
397
+ model,
398
+ output: [],
399
+ usage: null
400
+ }
401
+ });
402
+ // Accumulate content and tool calls for the final response
403
+ let accumulatedContent = "";
404
+ let accumulatedToolCalls = [];
405
+ let usage = null;
406
+ let currentToolCallIndex = -1;
407
+ try {
408
+ const body = upstreamResponse.body;
409
+ if (!body) {
410
+ throw new Error("No response body");
411
+ }
412
+ // Convert to async iterable
413
+ const reader = 'getReader' in body
414
+ ? body.getReader()
415
+ : null;
416
+ let buffer = "";
417
+ const processChunk = (text) => {
418
+ buffer += text;
419
+ const lines = buffer.split("\n");
420
+ buffer = lines.pop() || ""; // Keep incomplete line in buffer
421
+ for (const line of lines) {
422
+ if (line.startsWith("data: ")) {
423
+ const data = line.slice(6).trim();
424
+ if (data === "[DONE]") {
425
+ continue;
426
+ }
427
+ try {
428
+ const chunk = JSON.parse(data);
429
+ const delta = chunk.choices?.[0]?.delta;
430
+ if (delta) {
431
+ // Handle reasoning/thinking content delta
432
+ if (delta.reasoning_content || delta.reasoning) {
433
+ const reasoningDelta = delta.reasoning_content || delta.reasoning;
434
+ // Send reasoning delta event
435
+ sendEvent("response.reasoning_text.delta", {
436
+ type: "response.reasoning_text.delta",
437
+ output_index: 0,
438
+ content_index: 0,
439
+ delta: reasoningDelta
440
+ });
441
+ }
442
+ // Handle content delta
443
+ if (delta.content) {
444
+ accumulatedContent += delta.content;
445
+ // Send content delta event
446
+ sendEvent("response.output_text.delta", {
447
+ type: "response.output_text.delta",
448
+ output_index: 0,
449
+ content_index: 0,
450
+ delta: delta.content
451
+ });
452
+ }
453
+ // Handle tool calls delta
454
+ if (delta.tool_calls) {
455
+ for (const toolCallDelta of delta.tool_calls) {
456
+ const idx = toolCallDelta.index;
457
+ if (idx !== currentToolCallIndex) {
458
+ currentToolCallIndex = idx;
459
+ accumulatedToolCalls[idx] = {
460
+ id: toolCallDelta.id || "",
461
+ name: toolCallDelta.function?.name || "",
462
+ arguments: ""
463
+ };
464
+ }
465
+ if (toolCallDelta.id) {
466
+ accumulatedToolCalls[idx].id = toolCallDelta.id;
467
+ }
468
+ if (toolCallDelta.function?.name) {
469
+ accumulatedToolCalls[idx].name = toolCallDelta.function.name;
470
+ }
471
+ if (toolCallDelta.function?.arguments) {
472
+ accumulatedToolCalls[idx].arguments += toolCallDelta.function.arguments;
473
+ }
474
+ }
475
+ }
476
+ }
477
+ // Capture usage from final chunk
478
+ if (chunk.usage) {
479
+ usage = {
480
+ input_tokens: chunk.usage.prompt_tokens ?? 0,
481
+ output_tokens: chunk.usage.completion_tokens ?? 0,
482
+ total_tokens: chunk.usage.total_tokens ?? 0
483
+ };
484
+ }
485
+ }
486
+ catch (e) {
487
+ // Ignore parse errors for malformed chunks
488
+ }
489
+ }
490
+ }
491
+ };
492
+ if (reader) {
493
+ // Web Streams API (ReadableStream)
494
+ const decoder = new TextDecoder();
495
+ while (true) {
496
+ const { done, value } = await reader.read();
497
+ if (done)
498
+ break;
499
+ processChunk(decoder.decode(value, { stream: true }));
500
+ }
501
+ }
502
+ else {
503
+ // Node.js stream
504
+ const nodeStream = body;
505
+ for await (const chunk of nodeStream) {
506
+ processChunk(chunk.toString());
507
+ }
508
+ }
509
+ // Build final output
510
+ const output = [];
511
+ // Add tool calls first
512
+ for (const tc of accumulatedToolCalls) {
513
+ if (tc) {
514
+ output.push({
515
+ type: "function_call",
516
+ id: tc.id,
517
+ call_id: tc.id,
518
+ name: tc.name,
519
+ arguments: tc.arguments
520
+ });
521
+ // Send output_item.done for each tool call
522
+ sendEvent("response.output_item.done", {
523
+ type: "response.output_item.done",
524
+ output_index: output.length - 1,
525
+ item: output[output.length - 1]
526
+ });
527
+ }
528
+ }
529
+ // Add message content if any
530
+ if (accumulatedContent || output.length === 0) {
531
+ output.push({
532
+ type: "message",
533
+ role: "assistant",
534
+ content: [{ type: "output_text", text: accumulatedContent }]
535
+ });
536
+ // Send output_item.done for the message
537
+ sendEvent("response.output_item.done", {
538
+ type: "response.output_item.done",
539
+ output_index: output.length - 1,
540
+ item: output[output.length - 1]
541
+ });
542
+ }
543
+ // Send response.completed
544
+ sendEvent("response.completed", {
545
+ type: "response.completed",
546
+ response: {
547
+ id: requestId,
548
+ object: "response",
549
+ created_at: Math.floor(Date.now() / 1000),
550
+ model,
551
+ output,
552
+ usage
553
+ }
554
+ });
555
+ }
556
+ catch (error) {
557
+ console.error("[responses] Streaming error:", error);
558
+ // Send error as part of the stream
559
+ sendEvent("error", {
560
+ type: "error",
561
+ error: { message: error.message }
562
+ });
563
+ }
564
+ reply.raw.end();
565
+ }
566
+ /**
567
+ * Send response as Server-Sent Events in Responses format.
568
+ *
569
+ * Responses-style clients expect:
570
+ * - event: response.created
571
+ * - event: response.output_item.done (for each output item)
572
+ * - event: response.completed
573
+ *
574
+ * Each event has:
575
+ * - event: <event_type>
576
+ * - data: {"type":"<event_type>", ...payload}
577
+ */
578
+ async function sendAsSSE(reply, response) {
579
+ reply.raw.writeHead(200, {
580
+ "Content-Type": "text/event-stream",
581
+ "Cache-Control": "no-cache",
582
+ "Connection": "keep-alive"
583
+ });
584
+ // Helper to send an SSE event
585
+ const sendEvent = (eventType, data) => {
586
+ reply.raw.write(`event: ${eventType}\n`);
587
+ reply.raw.write(`data: ${JSON.stringify(data)}\n\n`);
588
+ };
589
+ // 1. response.created
590
+ sendEvent("response.created", {
591
+ type: "response.created",
592
+ response: {
593
+ id: response.id,
594
+ object: response.object,
595
+ created_at: response.created_at,
596
+ model: response.model,
597
+ output: [],
598
+ usage: null
599
+ }
600
+ });
601
+ // 2. response.output_item.done for each output item
602
+ for (let i = 0; i < response.output.length; i++) {
603
+ const item = response.output[i];
604
+ sendEvent("response.output_item.done", {
605
+ type: "response.output_item.done",
606
+ output_index: i,
607
+ item
608
+ });
609
+ }
610
+ // 3. response.completed
611
+ sendEvent("response.completed", {
612
+ type: "response.completed",
613
+ response: {
614
+ id: response.id,
615
+ object: response.object,
616
+ created_at: response.created_at,
617
+ model: response.model,
618
+ output: response.output,
619
+ usage: response.usage
620
+ }
621
+ });
622
+ reply.raw.end();
623
+ }
624
+ async function pickDefaultModel(paths) {
625
+ const smart = await (0, scheduler_1.selectPoolCandidates)(paths, "smart", {
626
+ requiredInput: ["text"],
627
+ requiredOutput: ["text"],
628
+ }, {
629
+ operation: "chat_completions",
630
+ stream: false,
631
+ });
632
+ if (smart && smart.candidates.length > 0) {
633
+ return "smart";
634
+ }
635
+ const byCapabilities = await (0, modelRegistry_1.pickBestProviderModelByCapabilities)(paths, { requiredInput: ["text"], requiredOutput: ["text"] }, "llm");
636
+ if (byCapabilities) {
637
+ return byCapabilities;
638
+ }
639
+ return null;
640
+ }
641
+ async function streamResponse(reply, response) {
642
+ const headers = normalizeHeaders(response.headers);
643
+ if (!headers["content-type"]) {
644
+ headers["content-type"] = "text/event-stream";
645
+ }
646
+ headers["cache-control"] = headers["cache-control"] ?? "no-cache";
647
+ reply.raw.writeHead(response.statusCode, headers);
648
+ await new Promise((resolve, reject) => {
649
+ (0, stream_1.pipeline)(response.body, reply.raw, (err) => {
650
+ if (err)
651
+ reject(err);
652
+ else
653
+ resolve();
654
+ });
655
+ });
656
+ }
657
+ function setHeaders(reply, headers) {
658
+ const normalized = normalizeHeaders(headers);
659
+ for (const [key, value] of Object.entries(normalized)) {
660
+ reply.header(key, value);
661
+ }
662
+ }
663
+ function normalizeHeaders(headers) {
664
+ const normalized = {};
665
+ for (const [key, value] of Object.entries(headers)) {
666
+ normalized[key.toLowerCase()] = Array.isArray(value) ? value.join(", ") : value;
667
+ }
668
+ return normalized;
669
+ }
670
+ async function readBody(response) {
671
+ const chunks = [];
672
+ for await (const chunk of response.body) {
673
+ chunks.push(Buffer.isBuffer(chunk) ? chunk : Buffer.from(chunk));
674
+ }
675
+ const buffer = Buffer.concat(chunks);
676
+ const contentType = normalizeHeaders(response.headers)["content-type"] ?? "";
677
+ if (contentType.includes("application/json")) {
678
+ try {
679
+ const payload = JSON.parse(buffer.toString("utf8"));
680
+ const usage = typeof payload === "object" && payload && payload.usage;
681
+ return {
682
+ payload,
683
+ totalTokens: usage?.total_tokens ?? null,
684
+ promptTokens: usage?.prompt_tokens ?? null,
685
+ completionTokens: usage?.completion_tokens ?? null,
686
+ };
687
+ }
688
+ catch {
689
+ return { payload: buffer, totalTokens: null, promptTokens: null, completionTokens: null };
690
+ }
691
+ }
692
+ return { payload: buffer, totalTokens: null, promptTokens: null, completionTokens: null };
693
+ }
694
+ function buildLog(requestId, model, outcome, latencyMs, stream, totalTokens) {
695
+ return {
696
+ requestId,
697
+ ts: new Date(),
698
+ route: {
699
+ publicModel: model,
700
+ endpointId: outcome.attempt.endpoint.id,
701
+ endpointName: outcome.attempt.endpoint.name,
702
+ upstreamModel: outcome.attempt.upstreamModel
703
+ },
704
+ request: { stream },
705
+ result: {
706
+ statusCode: outcome.attempt.response.statusCode,
707
+ latencyMs,
708
+ totalTokens: totalTokens ?? null
709
+ }
710
+ };
711
+ }