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.
- package/LICENSE +21 -0
- package/README.md +362 -0
- package/THIRD_PARTY_LICENSES.md +41 -0
- package/bin/mlx-bun.mjs +32 -0
- package/docs/curve-designer.html +303 -0
- package/docs/design/adapters-end-to-end.md +201 -0
- package/docs/design/compat-cli-surface-design.md +227 -0
- package/docs/design/docs-reorg-plan.md +152 -0
- package/docs/design/hlg-sampling.md +340 -0
- package/docs/design/memory-system.md +339 -0
- package/docs/design/optimization_plan.md +344 -0
- package/docs/design/parallel-slots.md +515 -0
- package/docs/design/runtime-isolation.md +247 -0
- package/docs/design/segmented-backward-training.md +508 -0
- package/docs/investigations/chunk-finetune-experiment.md +84 -0
- package/docs/investigations/curve-bisector-routing.md +123 -0
- package/docs/investigations/curve-runs/bisector-route.json +98 -0
- package/docs/investigations/curve-runs/bisector-trace.json +1393 -0
- package/docs/investigations/curve-runs/curve-terrain.json +1179 -0
- package/docs/investigations/curve-runs/curve-terrain.svg +42 -0
- package/docs/investigations/curve-terrain.html +105 -0
- package/docs/investigations/expert-offload-single-user-moe.md +292 -0
- package/docs/investigations/hlg-ab.html +34 -0
- package/docs/investigations/hlg-explorer.html +238 -0
- package/docs/investigations/hlg-figs/cliff.svg +1 -0
- package/docs/investigations/hlg-figs/cross.svg +1 -0
- package/docs/investigations/hlg-figs/edges.svg +1 -0
- package/docs/investigations/hlg-figs/k20-grids.svg +1 -0
- package/docs/investigations/hlg-figs/k20-peaks.svg +1 -0
- package/docs/investigations/hlg-figs/lw-curve.svg +1 -0
- package/docs/investigations/hlg-figs/terrain.svg +1 -0
- package/docs/investigations/hlg-report.html +262 -0
- package/docs/investigations/hlg-runs/hlg-ab.json +149 -0
- package/docs/investigations/hlg-runs/hlg-blind-prompts.md +87 -0
- package/docs/investigations/hlg-runs/hlg-blind.json +107 -0
- package/docs/investigations/hlg-runs/hlg-cliff.json +197 -0
- package/docs/investigations/hlg-runs/hlg-cross.json +505 -0
- package/docs/investigations/hlg-runs/hlg-edge.json +388 -0
- package/docs/investigations/hlg-runs/hlg-eval1.json +25 -0
- package/docs/investigations/hlg-runs/hlg-frontier.json +86 -0
- package/docs/investigations/hlg-runs/hlg-grid.json +242 -0
- package/docs/investigations/hlg-runs/hlg-grid2.json +343 -0
- package/docs/investigations/hlg-runs/hlg-lw-coarse.json +110 -0
- package/docs/investigations/hlg-runs/hlg-map.json +383 -0
- package/docs/investigations/hlg-runs/run0-original.log +159 -0
- package/docs/investigations/hlg-runs/runA-recommended.log +131 -0
- package/docs/investigations/hlg-runs/runAA-cliff-cpm5.log +26 -0
- package/docs/investigations/hlg-runs/runAB-grid2-e4b.log +18 -0
- package/docs/investigations/hlg-runs/runAC-peak-e4b-partial.log +25 -0
- package/docs/investigations/hlg-runs/runAD-ab-samples.log +8 -0
- package/docs/investigations/hlg-runs/runAE-edge-e4b.log +51 -0
- package/docs/investigations/hlg-runs/runAF-logit-calibration.log +10 -0
- package/docs/investigations/hlg-runs/runAG-eval-config2.log +16 -0
- package/docs/investigations/hlg-runs/runB-gain-variants.log +192 -0
- package/docs/investigations/hlg-runs/runC-pivots.log +162 -0
- package/docs/investigations/hlg-runs/runD-oetf.log +132 -0
- package/docs/investigations/hlg-runs/runE-eotf.log +162 -0
- package/docs/investigations/hlg-runs/runF-shaper-working.log +86 -0
- package/docs/investigations/hlg-runs/runG-shaper-sweep.log +101 -0
- package/docs/investigations/hlg-runs/runH-wsweep.log +176 -0
- package/docs/investigations/hlg-runs/runI-asweep.log +176 -0
- package/docs/investigations/hlg-runs/runJ-knobmap.log +49 -0
- package/docs/investigations/hlg-runs/runK-frontier.log +19 -0
- package/docs/investigations/hlg-runs/runL-widemap.log +65 -0
- package/docs/investigations/hlg-runs/runM-frontier-emb.log +15 -0
- package/docs/investigations/hlg-runs/runN-verify.log +116 -0
- package/docs/investigations/hlg-runs/runO-frontier-seed5000.log +15 -0
- package/docs/investigations/hlg-runs/runP-grid-AxSm.log +15 -0
- package/docs/investigations/hlg-runs/runQ-grid-tgW.log +15 -0
- package/docs/investigations/hlg-runs/runR-blind-transcript.log +79 -0
- package/docs/investigations/hlg-runs/runS-cross-coherence.log +33 -0
- package/docs/investigations/hlg-runs/runT-cross-cpm5.log +33 -0
- package/docs/investigations/hlg-runs/runU-cross-12b.log +33 -0
- package/docs/investigations/hlg-runs/runV-lw-e4b.log +22 -0
- package/docs/investigations/hlg-runs/runW-lw-12b.log +22 -0
- package/docs/investigations/hlg-runs/runX-lw-cpm5.log +22 -0
- package/docs/investigations/hlg-runs/runY-cliff-e4b.log +26 -0
- package/docs/investigations/hlg-runs/runZ-cliff-12b.log +26 -0
- package/docs/investigations/hlg-sampling-investigation.md +970 -0
- package/docs/investigations/lab-build-journal.md +157 -0
- package/docs/investigations/pi-builtin-investigation.md +308 -0
- package/docs/investigations/starter-model-port-handoff.md +323 -0
- package/docs/planning/IDEAS.md +26 -0
- package/docs/planning/PRODUCT_ROADMAP.md +426 -0
- package/docs/planning/ResearchTopics.md +119 -0
- package/docs/planning/chunk-task-roadmap.md +77 -0
- package/docs/planning/journal.md +155 -0
- package/docs/reference/distribution.md +121 -0
- package/docs/reference/embedding.md +168 -0
- package/docs/reference/library-api.md +153 -0
- package/docs/reference/server-api.md +224 -0
- package/docs/reference/server-config.md +243 -0
- package/docs/segmented-backward-handoff.md +148 -0
- package/package.json +68 -0
- package/src/anthropic.ts +491 -0
- package/src/chat-template.ts +186 -0
- package/src/cli.ts +1075 -0
- package/src/config.ts +229 -0
- package/src/curve-sampler.ts +122 -0
- package/src/dataset/generators.ts +1087 -0
- package/src/dataset/index.ts +12 -0
- package/src/dataset/job.ts +81 -0
- package/src/dataset/llm.ts +71 -0
- package/src/dataset/registry.ts +528 -0
- package/src/download.ts +283 -0
- package/src/eval/README.md +86 -0
- package/src/eval/kl-prompts.ts +29 -0
- package/src/eval/kl.ts +294 -0
- package/src/eval/quality-db.ts +92 -0
- package/src/eval/runner.ts +102 -0
- package/src/eval/score.ts +49 -0
- package/src/eval/tasks/bfcl.ts +500 -0
- package/src/eval/tasks/gsm8k.ts +85 -0
- package/src/eval/tasks/hashhop.ts +109 -0
- package/src/eval/tasks/humaneval.ts +252 -0
- package/src/eval/tasks/ifeval.ts +354 -0
- package/src/eval/tasks/mmlu.ts +145 -0
- package/src/evaldb.ts +199 -0
- package/src/expert-offload-build.ts +77 -0
- package/src/expert-offload.ts +61 -0
- package/src/expert-trace.ts +67 -0
- package/src/fit.ts +254 -0
- package/src/generate.ts +451 -0
- package/src/harness-pi.ts +177 -0
- package/src/hf-push.ts +628 -0
- package/src/index.ts +22 -0
- package/src/jobs/db.ts +139 -0
- package/src/jobs/index.ts +29 -0
- package/src/jobs/job-entry.ts +106 -0
- package/src/jobs/runner.ts +304 -0
- package/src/jobs/sse.ts +114 -0
- package/src/jobs/types.ts +58 -0
- package/src/kv-store.ts +185 -0
- package/src/lora.ts +314 -0
- package/src/md-modules.d.ts +7 -0
- package/src/mlx/array.ts +237 -0
- package/src/mlx/autograd.ts +267 -0
- package/src/mlx/checkpoint.ts +116 -0
- package/src/mlx/compile.ts +158 -0
- package/src/mlx/custom-vjp.ts +140 -0
- package/src/mlx/ffi.ts +433 -0
- package/src/mlx/metal-kernel.ts +130 -0
- package/src/mlx/ops.ts +707 -0
- package/src/mmap.ts +90 -0
- package/src/model/batched-mask.ts +204 -0
- package/src/model/batched-rotating.ts +300 -0
- package/src/model/compiled-decode.ts +681 -0
- package/src/model/factory.ts +27 -0
- package/src/model/fingerprint.ts +33 -0
- package/src/model/flash-attention.ts +674 -0
- package/src/model/fused-decode-kernel.ts +305 -0
- package/src/model/fused-geglu-kernel.ts +87 -0
- package/src/model/gemma4-base.ts +1616 -0
- package/src/model/gemma4.ts +1012 -0
- package/src/model/generated/gemma4-12b.ts +526 -0
- package/src/model/generated/gemma4-26b.ts +500 -0
- package/src/model/generated/gemma4-e4b.ts +757 -0
- package/src/model/generated/index.ts +18 -0
- package/src/model/minicpm5.ts +278 -0
- package/src/model/qwen3-delta.ts +224 -0
- package/src/model/qwen3_5.ts +429 -0
- package/src/model/support.ts +46 -0
- package/src/native-pack.ts +152 -0
- package/src/pi-provider.ts +95 -0
- package/src/pi-terminal.ts +264 -0
- package/src/pi-web.ts +924 -0
- package/src/preflight.ts +123 -0
- package/src/prompt-cache.ts +113 -0
- package/src/quantize/allocator.ts +627 -0
- package/src/quantize/calibration.ts +180 -0
- package/src/quantize/config-writer.ts +132 -0
- package/src/quantize/index.ts +57 -0
- package/src/quantize/job.ts +136 -0
- package/src/quantize/quantizer.ts +462 -0
- package/src/quantize/safetensors-writer.ts +171 -0
- package/src/quantize/sensitivity.ts +234 -0
- package/src/registry.ts +336 -0
- package/src/responses.ts +774 -0
- package/src/safetensors.ts +158 -0
- package/src/sampler.ts +517 -0
- package/src/serve/batch-scheduler.ts +362 -0
- package/src/serve/generation-gateway.ts +160 -0
- package/src/server.ts +1891 -0
- package/src/spec/drafter.ts +261 -0
- package/src/spec/generate.ts +221 -0
- package/src/status-page.html +497 -0
- package/src/tokenizer.ts +38 -0
- package/src/tool-call.ts +242 -0
- package/src/train/dataset.ts +461 -0
- package/src/train/export.ts +32 -0
- package/src/train/forward.ts +229 -0
- package/src/train/index.ts +43 -0
- package/src/train/job.ts +148 -0
- package/src/train/lora-params.ts +194 -0
- package/src/train/loss.ts +342 -0
- package/src/train/merge.ts +195 -0
- package/src/train/optimizer.ts +176 -0
- package/src/train/rank.ts +101 -0
- package/src/train/segmented.ts +520 -0
- package/src/train/trainer.ts +600 -0
- package/src/tui.ts +161 -0
- package/src/vision/embedder.ts +168 -0
- package/src/vision/preprocess.ts +247 -0
- package/src/vision/prompt.ts +158 -0
- package/src/vision/siglip.ts +599 -0
- package/src/web/app.html +2471 -0
- package/src/web/skills/web-research/SKILL.md +44 -0
- package/src/web/skills.ts +59 -0
- package/src/web-tools.ts +426 -0
- package/src/weights.ts +91 -0
package/src/responses.ts
ADDED
|
@@ -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
|
+
}
|