mlx-bun 0.0.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 (210) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +362 -0
  3. package/THIRD_PARTY_LICENSES.md +41 -0
  4. package/bin/mlx-bun.mjs +32 -0
  5. package/docs/curve-designer.html +303 -0
  6. package/docs/design/adapters-end-to-end.md +201 -0
  7. package/docs/design/compat-cli-surface-design.md +227 -0
  8. package/docs/design/docs-reorg-plan.md +152 -0
  9. package/docs/design/hlg-sampling.md +340 -0
  10. package/docs/design/memory-system.md +339 -0
  11. package/docs/design/optimization_plan.md +344 -0
  12. package/docs/design/parallel-slots.md +515 -0
  13. package/docs/design/runtime-isolation.md +247 -0
  14. package/docs/design/segmented-backward-training.md +508 -0
  15. package/docs/investigations/chunk-finetune-experiment.md +84 -0
  16. package/docs/investigations/curve-bisector-routing.md +123 -0
  17. package/docs/investigations/curve-runs/bisector-route.json +98 -0
  18. package/docs/investigations/curve-runs/bisector-trace.json +1393 -0
  19. package/docs/investigations/curve-runs/curve-terrain.json +1179 -0
  20. package/docs/investigations/curve-runs/curve-terrain.svg +42 -0
  21. package/docs/investigations/curve-terrain.html +105 -0
  22. package/docs/investigations/expert-offload-single-user-moe.md +292 -0
  23. package/docs/investigations/hlg-ab.html +34 -0
  24. package/docs/investigations/hlg-explorer.html +238 -0
  25. package/docs/investigations/hlg-figs/cliff.svg +1 -0
  26. package/docs/investigations/hlg-figs/cross.svg +1 -0
  27. package/docs/investigations/hlg-figs/edges.svg +1 -0
  28. package/docs/investigations/hlg-figs/k20-grids.svg +1 -0
  29. package/docs/investigations/hlg-figs/k20-peaks.svg +1 -0
  30. package/docs/investigations/hlg-figs/lw-curve.svg +1 -0
  31. package/docs/investigations/hlg-figs/terrain.svg +1 -0
  32. package/docs/investigations/hlg-report.html +262 -0
  33. package/docs/investigations/hlg-runs/hlg-ab.json +149 -0
  34. package/docs/investigations/hlg-runs/hlg-blind-prompts.md +87 -0
  35. package/docs/investigations/hlg-runs/hlg-blind.json +107 -0
  36. package/docs/investigations/hlg-runs/hlg-cliff.json +197 -0
  37. package/docs/investigations/hlg-runs/hlg-cross.json +505 -0
  38. package/docs/investigations/hlg-runs/hlg-edge.json +388 -0
  39. package/docs/investigations/hlg-runs/hlg-eval1.json +25 -0
  40. package/docs/investigations/hlg-runs/hlg-frontier.json +86 -0
  41. package/docs/investigations/hlg-runs/hlg-grid.json +242 -0
  42. package/docs/investigations/hlg-runs/hlg-grid2.json +343 -0
  43. package/docs/investigations/hlg-runs/hlg-lw-coarse.json +110 -0
  44. package/docs/investigations/hlg-runs/hlg-map.json +383 -0
  45. package/docs/investigations/hlg-runs/run0-original.log +159 -0
  46. package/docs/investigations/hlg-runs/runA-recommended.log +131 -0
  47. package/docs/investigations/hlg-runs/runAA-cliff-cpm5.log +26 -0
  48. package/docs/investigations/hlg-runs/runAB-grid2-e4b.log +18 -0
  49. package/docs/investigations/hlg-runs/runAC-peak-e4b-partial.log +25 -0
  50. package/docs/investigations/hlg-runs/runAD-ab-samples.log +8 -0
  51. package/docs/investigations/hlg-runs/runAE-edge-e4b.log +51 -0
  52. package/docs/investigations/hlg-runs/runAF-logit-calibration.log +10 -0
  53. package/docs/investigations/hlg-runs/runAG-eval-config2.log +16 -0
  54. package/docs/investigations/hlg-runs/runB-gain-variants.log +192 -0
  55. package/docs/investigations/hlg-runs/runC-pivots.log +162 -0
  56. package/docs/investigations/hlg-runs/runD-oetf.log +132 -0
  57. package/docs/investigations/hlg-runs/runE-eotf.log +162 -0
  58. package/docs/investigations/hlg-runs/runF-shaper-working.log +86 -0
  59. package/docs/investigations/hlg-runs/runG-shaper-sweep.log +101 -0
  60. package/docs/investigations/hlg-runs/runH-wsweep.log +176 -0
  61. package/docs/investigations/hlg-runs/runI-asweep.log +176 -0
  62. package/docs/investigations/hlg-runs/runJ-knobmap.log +49 -0
  63. package/docs/investigations/hlg-runs/runK-frontier.log +19 -0
  64. package/docs/investigations/hlg-runs/runL-widemap.log +65 -0
  65. package/docs/investigations/hlg-runs/runM-frontier-emb.log +15 -0
  66. package/docs/investigations/hlg-runs/runN-verify.log +116 -0
  67. package/docs/investigations/hlg-runs/runO-frontier-seed5000.log +15 -0
  68. package/docs/investigations/hlg-runs/runP-grid-AxSm.log +15 -0
  69. package/docs/investigations/hlg-runs/runQ-grid-tgW.log +15 -0
  70. package/docs/investigations/hlg-runs/runR-blind-transcript.log +79 -0
  71. package/docs/investigations/hlg-runs/runS-cross-coherence.log +33 -0
  72. package/docs/investigations/hlg-runs/runT-cross-cpm5.log +33 -0
  73. package/docs/investigations/hlg-runs/runU-cross-12b.log +33 -0
  74. package/docs/investigations/hlg-runs/runV-lw-e4b.log +22 -0
  75. package/docs/investigations/hlg-runs/runW-lw-12b.log +22 -0
  76. package/docs/investigations/hlg-runs/runX-lw-cpm5.log +22 -0
  77. package/docs/investigations/hlg-runs/runY-cliff-e4b.log +26 -0
  78. package/docs/investigations/hlg-runs/runZ-cliff-12b.log +26 -0
  79. package/docs/investigations/hlg-sampling-investigation.md +970 -0
  80. package/docs/investigations/lab-build-journal.md +157 -0
  81. package/docs/investigations/pi-builtin-investigation.md +308 -0
  82. package/docs/investigations/starter-model-port-handoff.md +323 -0
  83. package/docs/planning/IDEAS.md +26 -0
  84. package/docs/planning/PRODUCT_ROADMAP.md +426 -0
  85. package/docs/planning/ResearchTopics.md +119 -0
  86. package/docs/planning/chunk-task-roadmap.md +77 -0
  87. package/docs/planning/journal.md +155 -0
  88. package/docs/reference/distribution.md +121 -0
  89. package/docs/reference/embedding.md +168 -0
  90. package/docs/reference/library-api.md +153 -0
  91. package/docs/reference/server-api.md +224 -0
  92. package/docs/reference/server-config.md +243 -0
  93. package/docs/segmented-backward-handoff.md +148 -0
  94. package/package.json +68 -0
  95. package/src/anthropic.ts +491 -0
  96. package/src/chat-template.ts +186 -0
  97. package/src/cli.ts +1075 -0
  98. package/src/config.ts +229 -0
  99. package/src/curve-sampler.ts +122 -0
  100. package/src/dataset/generators.ts +1087 -0
  101. package/src/dataset/index.ts +12 -0
  102. package/src/dataset/job.ts +81 -0
  103. package/src/dataset/llm.ts +71 -0
  104. package/src/dataset/registry.ts +528 -0
  105. package/src/download.ts +283 -0
  106. package/src/eval/README.md +86 -0
  107. package/src/eval/kl-prompts.ts +29 -0
  108. package/src/eval/kl.ts +294 -0
  109. package/src/eval/quality-db.ts +92 -0
  110. package/src/eval/runner.ts +102 -0
  111. package/src/eval/score.ts +49 -0
  112. package/src/eval/tasks/bfcl.ts +500 -0
  113. package/src/eval/tasks/gsm8k.ts +85 -0
  114. package/src/eval/tasks/hashhop.ts +109 -0
  115. package/src/eval/tasks/humaneval.ts +252 -0
  116. package/src/eval/tasks/ifeval.ts +354 -0
  117. package/src/eval/tasks/mmlu.ts +145 -0
  118. package/src/evaldb.ts +199 -0
  119. package/src/expert-offload-build.ts +77 -0
  120. package/src/expert-offload.ts +61 -0
  121. package/src/expert-trace.ts +67 -0
  122. package/src/fit.ts +254 -0
  123. package/src/generate.ts +451 -0
  124. package/src/harness-pi.ts +177 -0
  125. package/src/hf-push.ts +628 -0
  126. package/src/index.ts +22 -0
  127. package/src/jobs/db.ts +139 -0
  128. package/src/jobs/index.ts +29 -0
  129. package/src/jobs/job-entry.ts +106 -0
  130. package/src/jobs/runner.ts +304 -0
  131. package/src/jobs/sse.ts +114 -0
  132. package/src/jobs/types.ts +58 -0
  133. package/src/kv-store.ts +185 -0
  134. package/src/lora.ts +314 -0
  135. package/src/md-modules.d.ts +7 -0
  136. package/src/mlx/array.ts +237 -0
  137. package/src/mlx/autograd.ts +267 -0
  138. package/src/mlx/checkpoint.ts +116 -0
  139. package/src/mlx/compile.ts +158 -0
  140. package/src/mlx/custom-vjp.ts +140 -0
  141. package/src/mlx/ffi.ts +433 -0
  142. package/src/mlx/metal-kernel.ts +130 -0
  143. package/src/mlx/ops.ts +707 -0
  144. package/src/mmap.ts +90 -0
  145. package/src/model/batched-mask.ts +204 -0
  146. package/src/model/batched-rotating.ts +300 -0
  147. package/src/model/compiled-decode.ts +681 -0
  148. package/src/model/factory.ts +27 -0
  149. package/src/model/fingerprint.ts +33 -0
  150. package/src/model/flash-attention.ts +674 -0
  151. package/src/model/fused-decode-kernel.ts +305 -0
  152. package/src/model/fused-geglu-kernel.ts +87 -0
  153. package/src/model/gemma4-base.ts +1616 -0
  154. package/src/model/gemma4.ts +1012 -0
  155. package/src/model/generated/gemma4-12b.ts +526 -0
  156. package/src/model/generated/gemma4-26b.ts +500 -0
  157. package/src/model/generated/gemma4-e4b.ts +757 -0
  158. package/src/model/generated/index.ts +18 -0
  159. package/src/model/minicpm5.ts +278 -0
  160. package/src/model/qwen3-delta.ts +224 -0
  161. package/src/model/qwen3_5.ts +429 -0
  162. package/src/model/support.ts +46 -0
  163. package/src/native-pack.ts +152 -0
  164. package/src/pi-provider.ts +95 -0
  165. package/src/pi-terminal.ts +264 -0
  166. package/src/pi-web.ts +924 -0
  167. package/src/preflight.ts +123 -0
  168. package/src/prompt-cache.ts +113 -0
  169. package/src/quantize/allocator.ts +627 -0
  170. package/src/quantize/calibration.ts +180 -0
  171. package/src/quantize/config-writer.ts +132 -0
  172. package/src/quantize/index.ts +57 -0
  173. package/src/quantize/job.ts +136 -0
  174. package/src/quantize/quantizer.ts +462 -0
  175. package/src/quantize/safetensors-writer.ts +171 -0
  176. package/src/quantize/sensitivity.ts +234 -0
  177. package/src/registry.ts +336 -0
  178. package/src/responses.ts +774 -0
  179. package/src/safetensors.ts +158 -0
  180. package/src/sampler.ts +517 -0
  181. package/src/serve/batch-scheduler.ts +362 -0
  182. package/src/serve/generation-gateway.ts +160 -0
  183. package/src/server.ts +1891 -0
  184. package/src/spec/drafter.ts +261 -0
  185. package/src/spec/generate.ts +221 -0
  186. package/src/status-page.html +497 -0
  187. package/src/tokenizer.ts +38 -0
  188. package/src/tool-call.ts +242 -0
  189. package/src/train/dataset.ts +461 -0
  190. package/src/train/export.ts +32 -0
  191. package/src/train/forward.ts +229 -0
  192. package/src/train/index.ts +43 -0
  193. package/src/train/job.ts +148 -0
  194. package/src/train/lora-params.ts +194 -0
  195. package/src/train/loss.ts +342 -0
  196. package/src/train/merge.ts +195 -0
  197. package/src/train/optimizer.ts +176 -0
  198. package/src/train/rank.ts +101 -0
  199. package/src/train/segmented.ts +520 -0
  200. package/src/train/trainer.ts +600 -0
  201. package/src/tui.ts +161 -0
  202. package/src/vision/embedder.ts +168 -0
  203. package/src/vision/preprocess.ts +247 -0
  204. package/src/vision/prompt.ts +158 -0
  205. package/src/vision/siglip.ts +599 -0
  206. package/src/web/app.html +2471 -0
  207. package/src/web/skills/web-research/SKILL.md +44 -0
  208. package/src/web/skills.ts +59 -0
  209. package/src/web-tools.ts +426 -0
  210. package/src/weights.ts +91 -0
@@ -0,0 +1,774 @@
1
+ // OpenAI Responses API translation layer + response store for the
2
+ // mlx-bun server (Phase 11).
3
+ //
4
+ // Ported from: mlx-optiq optiq/responses_shim.py (MIT) —
5
+ // responses_to_openai_body / openai_to_responses_response /
6
+ // ResponsesStreamTranslator / output_items_to_input_items — and
7
+ // optiq/response_store.py (ResponseStore: TTL-bounded, byte-capped
8
+ // LRU). The streaming event chain is the oracle's exactly; Codex
9
+ // hard-requires response.output_text.delta and response.completed,
10
+ // the rest are spec-compliance events for the broader SDK ecosystem.
11
+ //
12
+ // Documented deltas vs the oracle:
13
+ // - usage: real prompt/completion counts from our final chunk (it
14
+ // reads usage only from choice-less chunks and counts text chunks
15
+ // as output tokens); our prompt-cache hits populate
16
+ // input_tokens_details.cached_tokens (oracle hardcodes 0).
17
+ // - previous_response_id is echoed in the response when the request
18
+ // carried one (oracle always emits null).
19
+
20
+ // ---------------------------------------------------------------------
21
+ // Response store (previous_response_id resumption)
22
+ // ---------------------------------------------------------------------
23
+
24
+ export interface StoredResponse {
25
+ input: unknown[];
26
+ output: unknown[];
27
+ instructions: string | null;
28
+ }
29
+
30
+ interface StoreEntry extends StoredResponse {
31
+ createdAt: number;
32
+ size: number;
33
+ }
34
+
35
+ const DEFAULT_TTL_MS = 60 * 60 * 1000; // 1 hour — OpenAI's default
36
+ const DEFAULT_MAX_BYTES = 32 * 1024 * 1024;
37
+
38
+ /** Process-local TTL + byte-capped LRU (port of response_store.py).
39
+ * Does not survive restarts — sufficient for local serving; anything
40
+ * sturdier means external storage, out of scope upstream too. */
41
+ export class ResponseStore {
42
+ #items = new Map<string, StoreEntry>(); // Map iterates in insertion order
43
+ #bytes = 0;
44
+
45
+ constructor(
46
+ readonly ttlMs: number = DEFAULT_TTL_MS,
47
+ readonly maxBytes: number = DEFAULT_MAX_BYTES,
48
+ ) {}
49
+
50
+ get size(): number {
51
+ return this.#items.size;
52
+ }
53
+
54
+ get totalBytes(): number {
55
+ return this.#bytes;
56
+ }
57
+
58
+ put(id: string, entry: StoredResponse): void {
59
+ this.#evictExpired();
60
+ const prev = this.#items.get(id);
61
+ if (prev) {
62
+ this.#bytes -= prev.size;
63
+ this.#items.delete(id);
64
+ }
65
+ const size = JSON.stringify(entry.input).length + JSON.stringify(entry.output).length;
66
+ this.#items.set(id, {
67
+ input: [...entry.input],
68
+ output: [...entry.output],
69
+ instructions: entry.instructions,
70
+ createdAt: Date.now(),
71
+ size,
72
+ });
73
+ this.#bytes += size;
74
+ this.#evictLru();
75
+ }
76
+
77
+ get(id: string): StoredResponse | null {
78
+ this.#evictExpired();
79
+ const entry = this.#items.get(id);
80
+ if (!entry) return null;
81
+ // move-to-end (LRU touch)
82
+ this.#items.delete(id);
83
+ this.#items.set(id, entry);
84
+ return {
85
+ input: [...entry.input],
86
+ output: [...entry.output],
87
+ instructions: entry.instructions,
88
+ };
89
+ }
90
+
91
+ #evictExpired(): void {
92
+ const cutoff = Date.now() - this.ttlMs;
93
+ for (const [id, entry] of this.#items) {
94
+ if (entry.createdAt >= cutoff) break; // oldest-first walk, like the oracle
95
+ this.#bytes -= entry.size;
96
+ this.#items.delete(id);
97
+ }
98
+ }
99
+
100
+ #evictLru(): void {
101
+ for (const [id, entry] of this.#items) {
102
+ if (this.#bytes <= this.maxBytes) break;
103
+ this.#bytes -= entry.size;
104
+ this.#items.delete(id);
105
+ }
106
+ }
107
+ }
108
+
109
+ // ---------------------------------------------------------------------
110
+ // Request translation: Responses → Chat Completions
111
+ // ---------------------------------------------------------------------
112
+
113
+ export interface ResponsesRequest {
114
+ model?: string;
115
+ input?: string | Array<Record<string, unknown>>;
116
+ instructions?: string;
117
+ previous_response_id?: string;
118
+ max_output_tokens?: number;
119
+ max_tokens?: number;
120
+ temperature?: number;
121
+ top_p?: number;
122
+ top_k?: number;
123
+ stream?: boolean;
124
+ tools?: Array<Record<string, unknown>>;
125
+ tool_choice?: unknown;
126
+ }
127
+
128
+ const hex = () => crypto.randomUUID().replaceAll("-", "");
129
+ const genResponseId = () => `resp_${hex().slice(0, 24)}`;
130
+ const genCallId = () => `call_${hex().slice(0, 24)}`;
131
+ const genItemId = (prefix = "msg") => `${prefix}_${hex().slice(0, 24)}`;
132
+
133
+ const STATUS_MAP: Record<string, string> = {
134
+ stop: "completed",
135
+ length: "incomplete",
136
+ tool_calls: "completed",
137
+ function_call: "completed",
138
+ };
139
+
140
+ /** Flatten a Responses content array ({type, text} parts) to text. */
141
+ function coerceText(content: unknown): string {
142
+ if (content == null) return "";
143
+ if (typeof content === "string") return content;
144
+ if (!Array.isArray(content)) return String(content);
145
+ const parts: string[] = [];
146
+ for (const part of content) {
147
+ if (typeof part === "string") parts.push(part);
148
+ else if (part && typeof part === "object" && (part as any).text != null)
149
+ parts.push(String((part as any).text));
150
+ }
151
+ return parts.join("");
152
+ }
153
+
154
+ /** Convert a prior response's output array into input-shaped items for
155
+ * previous_response_id resumption (reasoning items dropped — replaying
156
+ * them invites the model to repeat itself). */
157
+ export function outputItemsToInputItems(output: unknown[]): Record<string, unknown>[] {
158
+ const items: Record<string, unknown>[] = [];
159
+ for (const raw of output ?? []) {
160
+ if (!raw || typeof raw !== "object") continue;
161
+ const item = raw as Record<string, unknown>;
162
+ if (item.type === "message") {
163
+ items.push({
164
+ type: "message",
165
+ role: item.role ?? "assistant",
166
+ content: item.content ?? [],
167
+ });
168
+ } else if (item.type === "function_call") {
169
+ items.push({
170
+ type: "function_call",
171
+ call_id: item.call_id ?? item.id,
172
+ name: item.name ?? "",
173
+ arguments: item.arguments ?? "",
174
+ });
175
+ }
176
+ }
177
+ return items;
178
+ }
179
+
180
+ /** Translate a Responses body into our chat-completions body shape.
181
+ * System text from `instructions` AND role=system/developer input
182
+ * items merge into ONE leading system message (strict templates
183
+ * reject duplicates; Codex sends both — oracle finding). */
184
+ export function responsesToChatBody(body: ResponsesRequest): Record<string, unknown> {
185
+ const systemParts: string[] = [];
186
+ const messages: Record<string, unknown>[] = [];
187
+ if (body.instructions) systemParts.push(String(body.instructions));
188
+
189
+ const inp = body.input;
190
+ if (typeof inp === "string") {
191
+ if (inp) messages.push({ role: "user", content: inp });
192
+ } else if (Array.isArray(inp)) {
193
+ for (const raw of inp) {
194
+ if (!raw || typeof raw !== "object") continue;
195
+ const item = raw as Record<string, unknown>;
196
+ if (item.type === "function_call") {
197
+ messages.push({
198
+ role: "assistant",
199
+ content: null,
200
+ tool_calls: [{
201
+ id: item.call_id ?? `call_${hex().slice(0, 12)}`,
202
+ type: "function",
203
+ function: { name: item.name, arguments: item.arguments ?? "" },
204
+ }],
205
+ });
206
+ continue;
207
+ }
208
+ if (item.type === "function_call_output") {
209
+ const output = item.output;
210
+ messages.push({
211
+ role: "tool",
212
+ tool_call_id: item.call_id ?? "",
213
+ content: typeof output === "string" ? output : JSON.stringify(output ?? ""),
214
+ });
215
+ continue;
216
+ }
217
+ // regular message item (explicit type or implicit by having role)
218
+ let role = String(item.role ?? "user");
219
+ const content = coerceText(item.content);
220
+ if (role === "system" || role === "developer") {
221
+ if (content) systemParts.push(content);
222
+ continue;
223
+ }
224
+ if (!["user", "assistant", "tool"].includes(role)) role = "user";
225
+ messages.push({ role, content });
226
+ }
227
+ }
228
+ if (systemParts.length) messages.unshift({ role: "system", content: systemParts.join("\n\n") });
229
+
230
+ const oai: Record<string, unknown> = { messages };
231
+ if (body.model != null) oai.model = body.model;
232
+ const maxTokens = body.max_output_tokens ?? body.max_tokens;
233
+ if (maxTokens != null) oai.max_tokens = maxTokens;
234
+ if (body.temperature != null) oai.temperature = body.temperature;
235
+ if (body.top_p != null) oai.top_p = body.top_p;
236
+ if (body.top_k != null) oai.top_k = body.top_k;
237
+ if (body.stream) oai.stream = true;
238
+
239
+ // Flat Responses tool shape → nested chat shape; built-ins
240
+ // (web_search, file_search, mcp, computer_use) dropped silently.
241
+ const tools = (body.tools ?? [])
242
+ .filter((t) => t && t.type === "function")
243
+ .map((t) => {
244
+ const fn: Record<string, unknown> = {};
245
+ for (const k of ["name", "description", "parameters", "strict"])
246
+ if (t[k] != null) fn[k] = t[k];
247
+ return { type: "function", function: fn };
248
+ });
249
+ if (tools.length) oai.tools = tools;
250
+
251
+ const tc = body.tool_choice;
252
+ if (tc != null) {
253
+ if (
254
+ typeof tc === "object" &&
255
+ (tc as any).type === "function" &&
256
+ (tc as any).name &&
257
+ !(tc as any).function
258
+ ) {
259
+ oai.tool_choice = { type: "function", function: { name: (tc as any).name } };
260
+ } else {
261
+ oai.tool_choice = tc; // "auto" / "none" / "required" pass through
262
+ }
263
+ }
264
+ return oai;
265
+ }
266
+
267
+ // ---------------------------------------------------------------------
268
+ // Response translation: chat completion → Responses (non-streaming)
269
+ // ---------------------------------------------------------------------
270
+
271
+ function baseResponse(model: string, id: string): Record<string, unknown> {
272
+ return {
273
+ id,
274
+ object: "response",
275
+ created_at: Math.floor(Date.now() / 1000),
276
+ status: "in_progress",
277
+ error: null,
278
+ incomplete_details: null,
279
+ instructions: null,
280
+ max_output_tokens: null,
281
+ model,
282
+ output: [] as unknown[],
283
+ parallel_tool_calls: true,
284
+ previous_response_id: null,
285
+ reasoning: null,
286
+ store: false,
287
+ temperature: null,
288
+ text: { format: { type: "text" } },
289
+ tool_choice: "auto",
290
+ tools: [],
291
+ top_p: null,
292
+ truncation: "disabled",
293
+ usage: null,
294
+ user: null,
295
+ metadata: {},
296
+ };
297
+ }
298
+
299
+ function usageBlock(
300
+ inputTokens: number,
301
+ outputTokens: number,
302
+ cachedTokens: number,
303
+ reasoningTokens: number,
304
+ ): Record<string, unknown> {
305
+ return {
306
+ input_tokens: inputTokens,
307
+ output_tokens: outputTokens,
308
+ total_tokens: inputTokens + outputTokens,
309
+ input_tokens_details: { cached_tokens: cachedTokens },
310
+ output_tokens_details: { reasoning_tokens: reasoningTokens },
311
+ };
312
+ }
313
+
314
+ /** Non-streaming: OpenAI chat.completion JSON → Responses JSON. */
315
+ export function chatJsonToResponses(
316
+ oai: any,
317
+ model: string,
318
+ previousResponseId: string | null = null,
319
+ ): Record<string, unknown> {
320
+ const choice = oai?.choices?.[0] ?? {};
321
+ const message = choice.message ?? {};
322
+ const text: string = message.content ?? "";
323
+ const reasoning: string = message.reasoning ?? "";
324
+ const output: Record<string, unknown>[] = [];
325
+ if (reasoning)
326
+ output.push({
327
+ id: genItemId("rs"),
328
+ type: "reasoning",
329
+ status: "completed",
330
+ summary: [{ type: "summary_text", text: reasoning }],
331
+ });
332
+ if (text)
333
+ output.push({
334
+ id: genItemId("msg"),
335
+ type: "message",
336
+ status: "completed",
337
+ role: "assistant",
338
+ content: [{ type: "output_text", text, annotations: [] }],
339
+ });
340
+ for (const tc of message.tool_calls ?? []) {
341
+ output.push({
342
+ id: genItemId("fc"),
343
+ type: "function_call",
344
+ status: "completed",
345
+ call_id: tc.id ?? genCallId(),
346
+ name: tc.function?.name ?? "",
347
+ arguments: tc.function?.arguments ?? "",
348
+ });
349
+ }
350
+ const usage = oai?.usage ?? {};
351
+ const resp = baseResponse(model, genResponseId());
352
+ resp.status = STATUS_MAP[choice.finish_reason ?? "stop"] ?? "completed";
353
+ resp.output = output;
354
+ resp.previous_response_id = previousResponseId;
355
+ resp.usage = usageBlock(
356
+ Number(usage.prompt_tokens ?? 0),
357
+ Number(usage.completion_tokens ?? 0),
358
+ Number(usage.prompt_tokens_details?.cached_tokens ?? 0),
359
+ reasoning ? Math.max(1, reasoning.split(/\s+/).length) : 0,
360
+ );
361
+ return resp;
362
+ }
363
+
364
+ // ---------------------------------------------------------------------
365
+ // Streaming: OpenAI chunks → Responses SSE events
366
+ // ---------------------------------------------------------------------
367
+
368
+ const sse = (event: string, data: unknown): string =>
369
+ `event: ${event}\ndata: ${JSON.stringify(data)}\n\n`;
370
+
371
+ interface ToolItem {
372
+ id: string;
373
+ callId: string;
374
+ name: string;
375
+ arguments: string;
376
+ outputIndex: number | null;
377
+ }
378
+
379
+ /** Incremental translator (oracle class of the same name). Feed chunks
380
+ * one at a time; frames flush per upstream chunk. finalResponse()
381
+ * returns the response.completed payload for the store. */
382
+ export class ResponsesStreamTranslator {
383
+ readonly respId = genResponseId();
384
+ readonly #base: Record<string, unknown>;
385
+ #headersSent = false;
386
+ #textItemId = genItemId("msg");
387
+ #textStarted = false;
388
+ #textAccum: string[] = [];
389
+ #reasoningItemId = genItemId("rs");
390
+ #reasoningStarted = false;
391
+ #reasoningClosed = false;
392
+ #reasoningOutputIndex: number | null = null;
393
+ #reasoningAccum: string[] = [];
394
+ #toolItems = new Map<number, ToolItem>();
395
+ #toolOrder: number[] = [];
396
+ #finishReason: string | null = null;
397
+ #inputTokens = 0;
398
+ #outputTokens = 0;
399
+ #cachedTokens = 0;
400
+ #outputIndex = 0;
401
+
402
+ constructor(readonly model: string, previousResponseId: string | null = null) {
403
+ this.#base = baseResponse(model, this.respId);
404
+ this.#base.previous_response_id = previousResponseId;
405
+ }
406
+
407
+ #headers(out: string[]): void {
408
+ if (this.#headersSent) return;
409
+ this.#headersSent = true;
410
+ out.push(
411
+ sse("response.created", { type: "response.created", response: this.#base }),
412
+ sse("response.in_progress", { type: "response.in_progress", response: this.#base }),
413
+ );
414
+ }
415
+
416
+ #closeReasoning(out: string[]): void {
417
+ if (!this.#reasoningStarted || this.#reasoningClosed) return;
418
+ this.#reasoningClosed = true;
419
+ const full = this.#reasoningAccum.join("");
420
+ out.push(
421
+ sse("response.reasoning_summary_text.done", {
422
+ type: "response.reasoning_summary_text.done",
423
+ item_id: this.#reasoningItemId,
424
+ output_index: this.#reasoningOutputIndex,
425
+ summary_index: 0,
426
+ text: full,
427
+ }),
428
+ sse("response.output_item.done", {
429
+ type: "response.output_item.done",
430
+ output_index: this.#reasoningOutputIndex,
431
+ item: {
432
+ id: this.#reasoningItemId,
433
+ type: "reasoning",
434
+ status: "completed",
435
+ summary: [{ type: "summary_text", text: full }],
436
+ },
437
+ }),
438
+ );
439
+ }
440
+
441
+ #closeTextItem(out: string[]): void {
442
+ const text = this.#textAccum.join("");
443
+ out.push(
444
+ sse("response.output_text.done", {
445
+ type: "response.output_text.done",
446
+ item_id: this.#textItemId,
447
+ output_index: this.#outputIndex,
448
+ content_index: 0,
449
+ text,
450
+ }),
451
+ sse("response.content_part.done", {
452
+ type: "response.content_part.done",
453
+ item_id: this.#textItemId,
454
+ output_index: this.#outputIndex,
455
+ content_index: 0,
456
+ part: { type: "output_text", text, annotations: [] },
457
+ }),
458
+ sse("response.output_item.done", {
459
+ type: "response.output_item.done",
460
+ output_index: this.#outputIndex,
461
+ item: {
462
+ id: this.#textItemId,
463
+ type: "message",
464
+ status: "completed",
465
+ role: "assistant",
466
+ content: [{ type: "output_text", text, annotations: [] }],
467
+ },
468
+ }),
469
+ );
470
+ this.#outputIndex++;
471
+ this.#textStarted = false;
472
+ }
473
+
474
+ addChunk(chunk: any): string[] {
475
+ const out: string[] = [];
476
+ this.#headers(out);
477
+
478
+ if (chunk?.usage) {
479
+ this.#inputTokens = Number(chunk.usage.prompt_tokens ?? this.#inputTokens);
480
+ this.#outputTokens = Number(chunk.usage.completion_tokens ?? this.#outputTokens);
481
+ this.#cachedTokens = Number(
482
+ chunk.usage.prompt_tokens_details?.cached_tokens ?? this.#cachedTokens,
483
+ );
484
+ }
485
+ const choice = chunk?.choices?.[0];
486
+ if (!choice) return out;
487
+ const delta = choice.delta ?? {};
488
+ if (choice.finish_reason) this.#finishReason = choice.finish_reason;
489
+
490
+ const reasoning = delta.reasoning;
491
+ if (reasoning) {
492
+ if (!this.#reasoningStarted) {
493
+ this.#reasoningStarted = true;
494
+ this.#reasoningOutputIndex = this.#outputIndex++;
495
+ out.push(
496
+ sse("response.output_item.added", {
497
+ type: "response.output_item.added",
498
+ output_index: this.#reasoningOutputIndex,
499
+ item: {
500
+ id: this.#reasoningItemId,
501
+ type: "reasoning",
502
+ status: "in_progress",
503
+ summary: [],
504
+ },
505
+ }),
506
+ );
507
+ }
508
+ this.#reasoningAccum.push(reasoning);
509
+ out.push(
510
+ sse("response.reasoning_summary_text.delta", {
511
+ type: "response.reasoning_summary_text.delta",
512
+ item_id: this.#reasoningItemId,
513
+ output_index: this.#reasoningOutputIndex,
514
+ summary_index: 0,
515
+ delta: reasoning,
516
+ }),
517
+ );
518
+ }
519
+
520
+ const text = delta.content;
521
+ if (text) {
522
+ this.#closeReasoning(out);
523
+ if (!this.#textStarted) {
524
+ this.#textStarted = true;
525
+ out.push(
526
+ sse("response.output_item.added", {
527
+ type: "response.output_item.added",
528
+ output_index: this.#outputIndex,
529
+ item: {
530
+ id: this.#textItemId,
531
+ type: "message",
532
+ status: "in_progress",
533
+ role: "assistant",
534
+ content: [],
535
+ },
536
+ }),
537
+ sse("response.content_part.added", {
538
+ type: "response.content_part.added",
539
+ item_id: this.#textItemId,
540
+ output_index: this.#outputIndex,
541
+ content_index: 0,
542
+ part: { type: "output_text", text: "", annotations: [] },
543
+ }),
544
+ );
545
+ }
546
+ this.#textAccum.push(text);
547
+ out.push(
548
+ sse("response.output_text.delta", {
549
+ type: "response.output_text.delta",
550
+ item_id: this.#textItemId,
551
+ output_index: this.#outputIndex,
552
+ content_index: 0,
553
+ delta: text,
554
+ }),
555
+ );
556
+ }
557
+
558
+ for (const tcDelta of delta.tool_calls ?? []) {
559
+ const idx = tcDelta.index ?? 0;
560
+ let entry = this.#toolItems.get(idx);
561
+ if (!entry) {
562
+ entry = {
563
+ id: genItemId("fc"),
564
+ callId: tcDelta.id ?? genCallId(),
565
+ name: "",
566
+ arguments: "",
567
+ outputIndex: null,
568
+ };
569
+ this.#toolItems.set(idx, entry);
570
+ this.#toolOrder.push(idx);
571
+ }
572
+ const fn = tcDelta.function ?? {};
573
+ if (fn.name) entry.name += fn.name;
574
+ const argsDelta = fn.arguments ?? "";
575
+ if (argsDelta) entry.arguments += argsDelta;
576
+
577
+ if (entry.outputIndex === null) {
578
+ if (this.#textStarted) this.#closeTextItem(out);
579
+ entry.outputIndex = this.#outputIndex++;
580
+ out.push(
581
+ sse("response.output_item.added", {
582
+ type: "response.output_item.added",
583
+ output_index: entry.outputIndex,
584
+ item: {
585
+ id: entry.id,
586
+ type: "function_call",
587
+ status: "in_progress",
588
+ call_id: entry.callId,
589
+ name: entry.name,
590
+ arguments: "",
591
+ },
592
+ }),
593
+ );
594
+ }
595
+ if (argsDelta) {
596
+ out.push(
597
+ sse("response.function_call_arguments.delta", {
598
+ type: "response.function_call_arguments.delta",
599
+ item_id: entry.id,
600
+ output_index: entry.outputIndex,
601
+ delta: argsDelta,
602
+ }),
603
+ );
604
+ }
605
+ }
606
+ return out;
607
+ }
608
+
609
+ #finalOutput(): Record<string, unknown>[] {
610
+ const out: Record<string, unknown>[] = [];
611
+ if (this.#reasoningStarted)
612
+ out.push({
613
+ id: this.#reasoningItemId,
614
+ type: "reasoning",
615
+ status: "completed",
616
+ summary: [{ type: "summary_text", text: this.#reasoningAccum.join("") }],
617
+ });
618
+ if (this.#textAccum.length)
619
+ out.push({
620
+ id: this.#textItemId,
621
+ type: "message",
622
+ status: "completed",
623
+ role: "assistant",
624
+ content: [{
625
+ type: "output_text",
626
+ text: this.#textAccum.join(""),
627
+ annotations: [],
628
+ }],
629
+ });
630
+ for (const idx of this.#toolOrder) {
631
+ const entry = this.#toolItems.get(idx)!;
632
+ out.push({
633
+ id: entry.id,
634
+ type: "function_call",
635
+ status: "completed",
636
+ call_id: entry.callId,
637
+ name: entry.name,
638
+ arguments: entry.arguments,
639
+ });
640
+ }
641
+ return out;
642
+ }
643
+
644
+ /** The response.completed payload (also what the store captures). */
645
+ finalResponse(): Record<string, unknown> {
646
+ return {
647
+ ...this.#base,
648
+ status: STATUS_MAP[this.#finishReason ?? "stop"] ?? "completed",
649
+ output: this.#finalOutput(),
650
+ usage: usageBlock(
651
+ this.#inputTokens,
652
+ this.#outputTokens,
653
+ this.#cachedTokens,
654
+ this.#reasoningStarted
655
+ ? Math.max(1, this.#reasoningAccum.join("").split(/\s+/).length)
656
+ : 0,
657
+ ),
658
+ };
659
+ }
660
+
661
+ finalize(): string[] {
662
+ const out: string[] = [];
663
+ this.#headers(out); // empty generation still gets created/in_progress
664
+ if (this.#textStarted) this.#closeTextItem(out);
665
+ this.#closeReasoning(out);
666
+ for (const idx of this.#toolOrder) {
667
+ const entry = this.#toolItems.get(idx)!;
668
+ out.push(
669
+ sse("response.function_call_arguments.done", {
670
+ type: "response.function_call_arguments.done",
671
+ item_id: entry.id,
672
+ output_index: entry.outputIndex,
673
+ arguments: entry.arguments,
674
+ }),
675
+ sse("response.output_item.done", {
676
+ type: "response.output_item.done",
677
+ output_index: entry.outputIndex,
678
+ item: {
679
+ id: entry.id,
680
+ type: "function_call",
681
+ status: "completed",
682
+ call_id: entry.callId,
683
+ name: entry.name,
684
+ arguments: entry.arguments,
685
+ },
686
+ }),
687
+ );
688
+ }
689
+ out.push(
690
+ sse("response.completed", {
691
+ type: "response.completed",
692
+ response: this.finalResponse(),
693
+ }),
694
+ );
695
+ return out;
696
+ }
697
+ }
698
+
699
+ /** Wrap our OpenAI SSE byte stream as a Responses SSE byte stream.
700
+ * onComplete fires after response.completed with the final payload so
701
+ * the route can store it for previous_response_id resumption. */
702
+ export function translateOpenAiSseToResponses(
703
+ upstream: ReadableStream<Uint8Array>,
704
+ model: string,
705
+ previousResponseId: string | null,
706
+ onComplete: (final: Record<string, unknown>) => void,
707
+ ): ReadableStream<Uint8Array> {
708
+ const translator = new ResponsesStreamTranslator(model, previousResponseId);
709
+ const enc = new TextEncoder();
710
+ const dec = new TextDecoder();
711
+ let buf = "";
712
+ let finalized = false;
713
+ return new ReadableStream<Uint8Array>({
714
+ async start(controller) {
715
+ const emit = (frames: string[]) => {
716
+ for (const f of frames) controller.enqueue(enc.encode(f));
717
+ };
718
+ const finish = () => {
719
+ if (finalized) return;
720
+ finalized = true;
721
+ emit(translator.finalize());
722
+ onComplete(translator.finalResponse());
723
+ };
724
+ const reader = upstream.getReader();
725
+ try {
726
+ while (true) {
727
+ const { done, value } = await reader.read();
728
+ if (done) break;
729
+ buf += dec.decode(value, { stream: true });
730
+ let sep: number;
731
+ while ((sep = buf.indexOf("\n\n")) !== -1) {
732
+ const frame = buf.slice(0, sep);
733
+ buf = buf.slice(sep + 2);
734
+ const data = frame
735
+ .split("\n")
736
+ .find((l) => l.startsWith("data: "))
737
+ ?.slice(6);
738
+ if (data == null) continue;
739
+ if (data === "[DONE]" || data === '"[DONE]"') {
740
+ finish();
741
+ continue;
742
+ }
743
+ const parsed = JSON.parse(data);
744
+ if (parsed?.error) {
745
+ emit([
746
+ sse("error", {
747
+ type: "error",
748
+ code: "server_error",
749
+ message: parsed.error.message ?? "generation failed",
750
+ param: null,
751
+ }),
752
+ ]);
753
+ continue;
754
+ }
755
+ emit(translator.addChunk(parsed));
756
+ }
757
+ }
758
+ finish();
759
+ } catch (e) {
760
+ emit([
761
+ sse("error", {
762
+ type: "error",
763
+ code: "server_error",
764
+ message: (e as Error).message,
765
+ param: null,
766
+ }),
767
+ ]);
768
+ } finally {
769
+ reader.releaseLock();
770
+ controller.close();
771
+ }
772
+ },
773
+ });
774
+ }