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/package.json
ADDED
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "mlx-bun",
|
|
3
|
+
"version": "0.0.4",
|
|
4
|
+
"description": "Native MLX inference for Bun on Apple Silicon — local LLM server (OpenAI/Anthropic-compatible) and TypeScript library. No Python.",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"license": "MIT",
|
|
7
|
+
"repository": {
|
|
8
|
+
"type": "git",
|
|
9
|
+
"url": "git+https://github.com/joshuarossi/mlx-bun.git"
|
|
10
|
+
},
|
|
11
|
+
"homepage": "https://github.com/joshuarossi/mlx-bun#readme",
|
|
12
|
+
"bugs": "https://github.com/joshuarossi/mlx-bun/issues",
|
|
13
|
+
"keywords": [
|
|
14
|
+
"mlx",
|
|
15
|
+
"llm",
|
|
16
|
+
"apple-silicon",
|
|
17
|
+
"metal",
|
|
18
|
+
"bun",
|
|
19
|
+
"local-inference",
|
|
20
|
+
"gemma",
|
|
21
|
+
"openai-compatible",
|
|
22
|
+
"anthropic"
|
|
23
|
+
],
|
|
24
|
+
"bin": {
|
|
25
|
+
"mlx-bun": "bin/mlx-bun.mjs"
|
|
26
|
+
},
|
|
27
|
+
"exports": {
|
|
28
|
+
".": "./src/index.ts",
|
|
29
|
+
"./package.json": "./package.json"
|
|
30
|
+
},
|
|
31
|
+
"files": [
|
|
32
|
+
"bin",
|
|
33
|
+
"src",
|
|
34
|
+
"docs",
|
|
35
|
+
"README.md",
|
|
36
|
+
"LICENSE",
|
|
37
|
+
"THIRD_PARTY_LICENSES.md"
|
|
38
|
+
],
|
|
39
|
+
"engines": {
|
|
40
|
+
"bun": ">=1.3.14"
|
|
41
|
+
},
|
|
42
|
+
"os": [
|
|
43
|
+
"darwin"
|
|
44
|
+
],
|
|
45
|
+
"cpu": [
|
|
46
|
+
"arm64"
|
|
47
|
+
],
|
|
48
|
+
"scripts": {
|
|
49
|
+
"test": "bash scripts/test.sh",
|
|
50
|
+
"link-cli": "chmod +x src/cli.ts && ln -sf $PWD/src/cli.ts $HOME/.bun/bin/mlx-bun && mlx-bun --version",
|
|
51
|
+
"build:binary": "sh scripts/build-binary.sh",
|
|
52
|
+
"release": "PUBLISH=1 sh scripts/release-binary.sh",
|
|
53
|
+
"publish:npm": "bun publish"
|
|
54
|
+
},
|
|
55
|
+
"devDependencies": {
|
|
56
|
+
"@anthropic-ai/sdk": "^0.104.1",
|
|
57
|
+
"@types/bun": "^1.3.14",
|
|
58
|
+
"openai": "^6.42.0"
|
|
59
|
+
},
|
|
60
|
+
"dependencies": {
|
|
61
|
+
"@earendil-works/pi-coding-agent": "0.79.4",
|
|
62
|
+
"@earendil-works/pi-ai": "0.79.4",
|
|
63
|
+
"@huggingface/jinja": "^0.5.9",
|
|
64
|
+
"@huggingface/tokenizers": "^0.1.3",
|
|
65
|
+
"fast-png": "^8.0.0",
|
|
66
|
+
"typebox": "1.1.38"
|
|
67
|
+
}
|
|
68
|
+
}
|
package/src/anthropic.ts
ADDED
|
@@ -0,0 +1,491 @@
|
|
|
1
|
+
// Anthropic Messages API translation layer for the mlx-bun server.
|
|
2
|
+
//
|
|
3
|
+
// Ported from: mlx-optiq optiq/anthropic_shim.py (MIT) —
|
|
4
|
+
// anthropic_to_openai_body / openai_to_anthropic_response /
|
|
5
|
+
// AnthropicStreamTranslator. The SSE event grammar is the oracle's
|
|
6
|
+
// exactly: message_start → [content_block_start/delta/stop]* →
|
|
7
|
+
// message_delta (stop_reason + output_tokens) → message_stop.
|
|
8
|
+
//
|
|
9
|
+
// Documented upgrades over the oracle (possible because our
|
|
10
|
+
// chat-completions layer is natively capable where mlx-lm's gemma path
|
|
11
|
+
// is not — same divergence class as the Phase 4 bidirectional-mask fix):
|
|
12
|
+
// - tools: Anthropic `tools`/`tool_use`/`tool_result` map onto our REAL
|
|
13
|
+
// OpenAI tools surface (token-level gemma tool calling) instead of
|
|
14
|
+
// the oracle's Qwen-style <tool_call> inline-text hack ("out of
|
|
15
|
+
// scope for v1" upstream). Streamed tool calls emit proper
|
|
16
|
+
// tool_use content blocks with input_json_delta.
|
|
17
|
+
// - images: Anthropic image blocks (base64 or url source) map to our
|
|
18
|
+
// image_url vision parts; the oracle emits "[image omitted]".
|
|
19
|
+
// - usage: real prompt/completion token counts from the final OpenAI
|
|
20
|
+
// chunk (the oracle counts text chunks); our prompt-cache hit count
|
|
21
|
+
// surfaces as cache_read_input_tokens.
|
|
22
|
+
// - prior-turn `thinking` blocks are dropped on re-ingest (Anthropic
|
|
23
|
+
// semantics) instead of the oracle's json.dumps fallback.
|
|
24
|
+
|
|
25
|
+
import type { ChatMessage, ToolDefinition } from "./chat-template";
|
|
26
|
+
|
|
27
|
+
// ---------------------------------------------------------------------
|
|
28
|
+
// Request translation: Anthropic → OpenAI (our ChatRequest shape)
|
|
29
|
+
// ---------------------------------------------------------------------
|
|
30
|
+
|
|
31
|
+
interface AnthropicContentBlock {
|
|
32
|
+
type?: string;
|
|
33
|
+
text?: string;
|
|
34
|
+
thinking?: string;
|
|
35
|
+
id?: string;
|
|
36
|
+
name?: string;
|
|
37
|
+
input?: Record<string, unknown>;
|
|
38
|
+
tool_use_id?: string;
|
|
39
|
+
content?: unknown;
|
|
40
|
+
source?: { type?: string; media_type?: string; data?: string; url?: string };
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
export interface AnthropicRequest {
|
|
44
|
+
model?: string;
|
|
45
|
+
system?: string | AnthropicContentBlock[];
|
|
46
|
+
messages?: Array<{ role?: string; content?: string | AnthropicContentBlock[] }>;
|
|
47
|
+
max_tokens?: number;
|
|
48
|
+
temperature?: number;
|
|
49
|
+
top_p?: number;
|
|
50
|
+
top_k?: number;
|
|
51
|
+
stop_sequences?: string[];
|
|
52
|
+
stream?: boolean;
|
|
53
|
+
tools?: Array<{
|
|
54
|
+
name?: string;
|
|
55
|
+
description?: string;
|
|
56
|
+
input_schema?: Record<string, unknown>;
|
|
57
|
+
type?: string;
|
|
58
|
+
}>;
|
|
59
|
+
tool_choice?: { type?: string };
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
/** Flatten text-bearing blocks (used for system and tool_result content). */
|
|
63
|
+
function flattenText(content: unknown): string {
|
|
64
|
+
if (content == null) return "";
|
|
65
|
+
if (typeof content === "string") return content;
|
|
66
|
+
if (!Array.isArray(content)) return String(content);
|
|
67
|
+
const parts: string[] = [];
|
|
68
|
+
for (const block of content as AnthropicContentBlock[]) {
|
|
69
|
+
if (typeof block !== "object" || block === null) parts.push(String(block));
|
|
70
|
+
else if (block.type === "text") parts.push(block.text ?? "");
|
|
71
|
+
}
|
|
72
|
+
return parts.join("\n");
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
function imagePartFromBlock(block: AnthropicContentBlock): Record<string, unknown> | null {
|
|
76
|
+
const src = block.source;
|
|
77
|
+
if (!src) return null;
|
|
78
|
+
if (src.type === "base64" && src.data)
|
|
79
|
+
return {
|
|
80
|
+
type: "image_url",
|
|
81
|
+
image_url: { url: `data:${src.media_type ?? "image/png"};base64,${src.data}` },
|
|
82
|
+
};
|
|
83
|
+
if (src.type === "url" && src.url)
|
|
84
|
+
return { type: "image_url", image_url: { url: src.url } };
|
|
85
|
+
return null;
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
/** Translate an Anthropic /v1/messages body into the OpenAI
|
|
89
|
+
* /v1/chat/completions body our server consumes. Throws on
|
|
90
|
+
* structurally invalid input (caller maps to invalid_request_error). */
|
|
91
|
+
export function anthropicToChatBody(body: AnthropicRequest): Record<string, unknown> {
|
|
92
|
+
if (!Array.isArray(body.messages) || body.messages.length === 0)
|
|
93
|
+
throw new Error("messages: at least one message is required");
|
|
94
|
+
|
|
95
|
+
const messages: ChatMessage[] = [];
|
|
96
|
+
if (body.system != null) {
|
|
97
|
+
const sys = typeof body.system === "string" ? body.system : flattenText(body.system);
|
|
98
|
+
if (sys) messages.push({ role: "system", content: sys });
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
for (const m of body.messages) {
|
|
102
|
+
const role = m.role === "assistant" ? "assistant" : "user";
|
|
103
|
+
if (typeof m.content === "string" || m.content == null) {
|
|
104
|
+
messages.push({ role, content: m.content ?? "" });
|
|
105
|
+
continue;
|
|
106
|
+
}
|
|
107
|
+
const textParts: string[] = [];
|
|
108
|
+
const imageParts: Record<string, unknown>[] = [];
|
|
109
|
+
const toolCalls: NonNullable<ChatMessage["tool_calls"]> = [];
|
|
110
|
+
for (const block of m.content) {
|
|
111
|
+
switch (block.type) {
|
|
112
|
+
case "text":
|
|
113
|
+
textParts.push(block.text ?? "");
|
|
114
|
+
break;
|
|
115
|
+
case "thinking":
|
|
116
|
+
break; // prior-turn thinking is not re-fed (Anthropic semantics)
|
|
117
|
+
case "image": {
|
|
118
|
+
const part = imagePartFromBlock(block);
|
|
119
|
+
if (part) imageParts.push(part);
|
|
120
|
+
break;
|
|
121
|
+
}
|
|
122
|
+
case "tool_use":
|
|
123
|
+
toolCalls.push({
|
|
124
|
+
id: block.id ?? `toolu_${crypto.randomUUID().slice(0, 12)}`,
|
|
125
|
+
type: "function",
|
|
126
|
+
function: {
|
|
127
|
+
name: block.name ?? "",
|
|
128
|
+
arguments: JSON.stringify(block.input ?? {}),
|
|
129
|
+
},
|
|
130
|
+
});
|
|
131
|
+
break;
|
|
132
|
+
case "tool_result":
|
|
133
|
+
// Anthropic packs tool results into the next user message;
|
|
134
|
+
// OpenAI (and the gemma template) want role:"tool" messages.
|
|
135
|
+
messages.push({
|
|
136
|
+
role: "tool",
|
|
137
|
+
tool_call_id: block.tool_use_id,
|
|
138
|
+
content: flattenText(block.content),
|
|
139
|
+
});
|
|
140
|
+
break;
|
|
141
|
+
default:
|
|
142
|
+
if (block.text) textParts.push(block.text);
|
|
143
|
+
}
|
|
144
|
+
}
|
|
145
|
+
if (role === "assistant") {
|
|
146
|
+
const msg: ChatMessage = { role, content: textParts.join("\n") || null };
|
|
147
|
+
if (toolCalls.length) msg.tool_calls = toolCalls;
|
|
148
|
+
if (msg.content || toolCalls.length) messages.push(msg);
|
|
149
|
+
} else if (imageParts.length) {
|
|
150
|
+
messages.push({
|
|
151
|
+
role,
|
|
152
|
+
content: [
|
|
153
|
+
...textParts.filter((t) => t).map((t) => ({ type: "text", text: t })),
|
|
154
|
+
...imageParts,
|
|
155
|
+
],
|
|
156
|
+
});
|
|
157
|
+
} else if (textParts.length) {
|
|
158
|
+
messages.push({ role, content: textParts.join("\n") });
|
|
159
|
+
}
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
const oai: Record<string, unknown> = { messages };
|
|
163
|
+
if (body.model != null) oai.model = body.model;
|
|
164
|
+
if (body.max_tokens != null) oai.max_tokens = body.max_tokens;
|
|
165
|
+
if (body.temperature != null) oai.temperature = body.temperature;
|
|
166
|
+
if (body.top_p != null) oai.top_p = body.top_p;
|
|
167
|
+
if (body.top_k != null) oai.top_k = body.top_k;
|
|
168
|
+
if (body.stop_sequences?.length) oai.stop = body.stop_sequences;
|
|
169
|
+
if (body.stream) oai.stream = true;
|
|
170
|
+
|
|
171
|
+
// Client tools only (entries with name + input_schema); server-tool
|
|
172
|
+
// types (web_search etc.) have no local meaning and are dropped.
|
|
173
|
+
const tools: ToolDefinition[] = (body.tools ?? [])
|
|
174
|
+
.filter((t) => t.name && t.input_schema)
|
|
175
|
+
.map((t) => ({
|
|
176
|
+
type: "function" as const,
|
|
177
|
+
function: {
|
|
178
|
+
name: t.name!,
|
|
179
|
+
description: t.description,
|
|
180
|
+
parameters: t.input_schema,
|
|
181
|
+
},
|
|
182
|
+
}));
|
|
183
|
+
if (tools.length) oai.tools = tools;
|
|
184
|
+
if (body.tool_choice?.type === "none") oai.tool_choice = "none";
|
|
185
|
+
|
|
186
|
+
return oai;
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
// ---------------------------------------------------------------------
|
|
190
|
+
// Response translation: OpenAI → Anthropic
|
|
191
|
+
// ---------------------------------------------------------------------
|
|
192
|
+
|
|
193
|
+
const STOP_REASON_MAP: Record<string, string> = {
|
|
194
|
+
stop: "end_turn",
|
|
195
|
+
length: "max_tokens",
|
|
196
|
+
tool_calls: "tool_use",
|
|
197
|
+
function_call: "tool_use",
|
|
198
|
+
};
|
|
199
|
+
|
|
200
|
+
const genMessageId = () => `msg_${crypto.randomUUID().replaceAll("-", "").slice(0, 24)}`;
|
|
201
|
+
|
|
202
|
+
function usageFromOpenAi(usage: any): Record<string, number> {
|
|
203
|
+
const out: Record<string, number> = {
|
|
204
|
+
input_tokens: Number(usage?.prompt_tokens ?? 0),
|
|
205
|
+
output_tokens: Number(usage?.completion_tokens ?? 0),
|
|
206
|
+
};
|
|
207
|
+
const cached = usage?.prompt_tokens_details?.cached_tokens;
|
|
208
|
+
if (cached) out.cache_read_input_tokens = Number(cached);
|
|
209
|
+
return out;
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
/** Non-streaming: OpenAI chat.completion JSON → Anthropic message JSON. */
|
|
213
|
+
export function chatJsonToAnthropic(oai: any, model: string): Record<string, unknown> {
|
|
214
|
+
const choice = oai?.choices?.[0] ?? {};
|
|
215
|
+
const message = choice.message ?? {};
|
|
216
|
+
const content: Record<string, unknown>[] = [];
|
|
217
|
+
if (message.reasoning)
|
|
218
|
+
content.push({ type: "thinking", thinking: message.reasoning, signature: "" });
|
|
219
|
+
if (message.content) content.push({ type: "text", text: message.content });
|
|
220
|
+
let finish: string | null = choice.finish_reason ?? null;
|
|
221
|
+
for (const tc of message.tool_calls ?? []) {
|
|
222
|
+
let input: unknown;
|
|
223
|
+
try {
|
|
224
|
+
input = JSON.parse(tc.function?.arguments ?? "{}");
|
|
225
|
+
} catch {
|
|
226
|
+
input = { _raw: tc.function?.arguments ?? "" };
|
|
227
|
+
}
|
|
228
|
+
content.push({
|
|
229
|
+
type: "tool_use",
|
|
230
|
+
id: tc.id ?? `toolu_${crypto.randomUUID().slice(0, 12)}`,
|
|
231
|
+
name: tc.function?.name,
|
|
232
|
+
input,
|
|
233
|
+
});
|
|
234
|
+
finish = "tool_calls";
|
|
235
|
+
}
|
|
236
|
+
return {
|
|
237
|
+
id: genMessageId(),
|
|
238
|
+
type: "message",
|
|
239
|
+
role: "assistant",
|
|
240
|
+
content,
|
|
241
|
+
model,
|
|
242
|
+
stop_reason: STOP_REASON_MAP[finish ?? "stop"] ?? "end_turn",
|
|
243
|
+
stop_sequence: null,
|
|
244
|
+
usage: usageFromOpenAi(oai?.usage),
|
|
245
|
+
};
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
// ---------------------------------------------------------------------
|
|
249
|
+
// Streaming: OpenAI chunk events → Anthropic SSE event frames
|
|
250
|
+
// ---------------------------------------------------------------------
|
|
251
|
+
|
|
252
|
+
const sse = (event: string, data: unknown): string =>
|
|
253
|
+
`event: ${event}\ndata: ${JSON.stringify(data)}\n\n`;
|
|
254
|
+
|
|
255
|
+
/** Incremental translator (oracle class of the same name). Feed OpenAI
|
|
256
|
+
* chunk objects one at a time; each call returns the Anthropic SSE
|
|
257
|
+
* frames to flush BEFORE the next upstream token arrives (first-token
|
|
258
|
+
* latency on a laptop blows SDK timeouts otherwise — oracle finding). */
|
|
259
|
+
export class AnthropicStreamTranslator {
|
|
260
|
+
readonly msgId = genMessageId();
|
|
261
|
+
#headersSent = false;
|
|
262
|
+
#thinkingStarted = false;
|
|
263
|
+
#thinkingClosed = false;
|
|
264
|
+
#textStarted = false;
|
|
265
|
+
#textIndex = 0;
|
|
266
|
+
#nextIndex = 0;
|
|
267
|
+
#stopReason = "end_turn";
|
|
268
|
+
#countedTokens = 0;
|
|
269
|
+
#usage: Record<string, number> | null = null;
|
|
270
|
+
|
|
271
|
+
constructor(readonly model: string) {}
|
|
272
|
+
|
|
273
|
+
#headers(out: string[]): void {
|
|
274
|
+
if (this.#headersSent) return;
|
|
275
|
+
this.#headersSent = true;
|
|
276
|
+
out.push(
|
|
277
|
+
sse("message_start", {
|
|
278
|
+
type: "message_start",
|
|
279
|
+
message: {
|
|
280
|
+
id: this.msgId, type: "message", role: "assistant",
|
|
281
|
+
model: this.model, content: [],
|
|
282
|
+
stop_reason: null, stop_sequence: null,
|
|
283
|
+
usage: { input_tokens: 0, output_tokens: 0 },
|
|
284
|
+
},
|
|
285
|
+
}),
|
|
286
|
+
);
|
|
287
|
+
}
|
|
288
|
+
|
|
289
|
+
#closeThinking(out: string[]): void {
|
|
290
|
+
out.push(
|
|
291
|
+
sse("content_block_delta", {
|
|
292
|
+
type: "content_block_delta",
|
|
293
|
+
index: 0,
|
|
294
|
+
// unsigned local reasoning: empty signature (spec allows it)
|
|
295
|
+
delta: { type: "signature_delta", signature: "" },
|
|
296
|
+
}),
|
|
297
|
+
sse("content_block_stop", { type: "content_block_stop", index: 0 }),
|
|
298
|
+
);
|
|
299
|
+
this.#thinkingClosed = true;
|
|
300
|
+
}
|
|
301
|
+
|
|
302
|
+
#closeText(out: string[]): void {
|
|
303
|
+
out.push(sse("content_block_stop", { type: "content_block_stop", index: this.#textIndex }));
|
|
304
|
+
this.#textStarted = false;
|
|
305
|
+
}
|
|
306
|
+
|
|
307
|
+
addChunk(chunk: any): string[] {
|
|
308
|
+
const out: string[] = [];
|
|
309
|
+
this.#headers(out);
|
|
310
|
+
const choice = chunk?.choices?.[0] ?? {};
|
|
311
|
+
const delta = choice.delta ?? {};
|
|
312
|
+
const fin = choice.finish_reason;
|
|
313
|
+
|
|
314
|
+
const reasoning = delta.reasoning;
|
|
315
|
+
if (reasoning) {
|
|
316
|
+
if (!this.#thinkingStarted) {
|
|
317
|
+
this.#thinkingStarted = true;
|
|
318
|
+
this.#nextIndex = 1;
|
|
319
|
+
out.push(
|
|
320
|
+
sse("content_block_start", {
|
|
321
|
+
type: "content_block_start",
|
|
322
|
+
index: 0,
|
|
323
|
+
content_block: { type: "thinking", thinking: "" },
|
|
324
|
+
}),
|
|
325
|
+
);
|
|
326
|
+
}
|
|
327
|
+
out.push(
|
|
328
|
+
sse("content_block_delta", {
|
|
329
|
+
type: "content_block_delta",
|
|
330
|
+
index: 0,
|
|
331
|
+
delta: { type: "thinking_delta", thinking: reasoning },
|
|
332
|
+
}),
|
|
333
|
+
);
|
|
334
|
+
}
|
|
335
|
+
|
|
336
|
+
const text = delta.content;
|
|
337
|
+
if (text) {
|
|
338
|
+
if (this.#thinkingStarted && !this.#thinkingClosed) this.#closeThinking(out);
|
|
339
|
+
if (!this.#textStarted) {
|
|
340
|
+
this.#textStarted = true;
|
|
341
|
+
this.#textIndex = this.#nextIndex++;
|
|
342
|
+
out.push(
|
|
343
|
+
sse("content_block_start", {
|
|
344
|
+
type: "content_block_start",
|
|
345
|
+
index: this.#textIndex,
|
|
346
|
+
content_block: { type: "text", text: "" },
|
|
347
|
+
}),
|
|
348
|
+
);
|
|
349
|
+
}
|
|
350
|
+
out.push(
|
|
351
|
+
sse("content_block_delta", {
|
|
352
|
+
type: "content_block_delta",
|
|
353
|
+
index: this.#textIndex,
|
|
354
|
+
delta: { type: "text_delta", text },
|
|
355
|
+
}),
|
|
356
|
+
);
|
|
357
|
+
this.#countedTokens++;
|
|
358
|
+
}
|
|
359
|
+
|
|
360
|
+
// Our server emits structured tool calls in one delta chunk; each
|
|
361
|
+
// becomes a complete tool_use block (start → input_json_delta → stop).
|
|
362
|
+
for (const tc of delta.tool_calls ?? []) {
|
|
363
|
+
if (this.#thinkingStarted && !this.#thinkingClosed) this.#closeThinking(out);
|
|
364
|
+
if (this.#textStarted) this.#closeText(out);
|
|
365
|
+
const idx = this.#nextIndex++;
|
|
366
|
+
out.push(
|
|
367
|
+
sse("content_block_start", {
|
|
368
|
+
type: "content_block_start",
|
|
369
|
+
index: idx,
|
|
370
|
+
content_block: {
|
|
371
|
+
type: "tool_use",
|
|
372
|
+
id: tc.id ?? `toolu_${crypto.randomUUID().slice(0, 12)}`,
|
|
373
|
+
name: tc.function?.name ?? "",
|
|
374
|
+
input: {},
|
|
375
|
+
},
|
|
376
|
+
}),
|
|
377
|
+
sse("content_block_delta", {
|
|
378
|
+
type: "content_block_delta",
|
|
379
|
+
index: idx,
|
|
380
|
+
delta: { type: "input_json_delta", partial_json: tc.function?.arguments ?? "{}" },
|
|
381
|
+
}),
|
|
382
|
+
sse("content_block_stop", { type: "content_block_stop", index: idx }),
|
|
383
|
+
);
|
|
384
|
+
this.#stopReason = "tool_use";
|
|
385
|
+
}
|
|
386
|
+
|
|
387
|
+
if (fin) {
|
|
388
|
+
if (this.#stopReason !== "tool_use")
|
|
389
|
+
this.#stopReason = STOP_REASON_MAP[fin] ?? "end_turn";
|
|
390
|
+
if (chunk.usage) this.#usage = usageFromOpenAi(chunk.usage);
|
|
391
|
+
}
|
|
392
|
+
return out;
|
|
393
|
+
}
|
|
394
|
+
|
|
395
|
+
finalize(): string[] {
|
|
396
|
+
const out: string[] = [];
|
|
397
|
+
this.#headers(out);
|
|
398
|
+
if (this.#thinkingStarted && !this.#thinkingClosed) this.#closeThinking(out);
|
|
399
|
+
if (this.#textStarted) this.#closeText(out);
|
|
400
|
+
else if (!this.#thinkingStarted && this.#nextIndex === 0) {
|
|
401
|
+
// empty generation: emit one empty text block (oracle behavior)
|
|
402
|
+
out.push(
|
|
403
|
+
sse("content_block_start", {
|
|
404
|
+
type: "content_block_start",
|
|
405
|
+
index: 0,
|
|
406
|
+
content_block: { type: "text", text: "" },
|
|
407
|
+
}),
|
|
408
|
+
sse("content_block_stop", { type: "content_block_stop", index: 0 }),
|
|
409
|
+
);
|
|
410
|
+
}
|
|
411
|
+
out.push(
|
|
412
|
+
sse("message_delta", {
|
|
413
|
+
type: "message_delta",
|
|
414
|
+
delta: { stop_reason: this.#stopReason, stop_sequence: null },
|
|
415
|
+
// real usage from the final OpenAI chunk when present; the
|
|
416
|
+
// oracle's chunk count is the fallback
|
|
417
|
+
usage: this.#usage ?? { output_tokens: this.#countedTokens },
|
|
418
|
+
}),
|
|
419
|
+
sse("message_stop", { type: "message_stop" }),
|
|
420
|
+
);
|
|
421
|
+
return out;
|
|
422
|
+
}
|
|
423
|
+
}
|
|
424
|
+
|
|
425
|
+
/** Wrap our OpenAI SSE byte stream as an Anthropic SSE byte stream.
|
|
426
|
+
* Frames translate as they arrive (per-chunk flush preserved — the
|
|
427
|
+
* upstream already rate-limits socket flushes). */
|
|
428
|
+
export function translateOpenAiSse(
|
|
429
|
+
upstream: ReadableStream<Uint8Array>,
|
|
430
|
+
model: string,
|
|
431
|
+
): ReadableStream<Uint8Array> {
|
|
432
|
+
const translator = new AnthropicStreamTranslator(model);
|
|
433
|
+
const enc = new TextEncoder();
|
|
434
|
+
const dec = new TextDecoder();
|
|
435
|
+
let buf = "";
|
|
436
|
+
let finalized = false;
|
|
437
|
+
return new ReadableStream<Uint8Array>({
|
|
438
|
+
async start(controller) {
|
|
439
|
+
const emit = (frames: string[]) => {
|
|
440
|
+
for (const f of frames) controller.enqueue(enc.encode(f));
|
|
441
|
+
};
|
|
442
|
+
const reader = upstream.getReader();
|
|
443
|
+
try {
|
|
444
|
+
while (true) {
|
|
445
|
+
const { done, value } = await reader.read();
|
|
446
|
+
if (done) break;
|
|
447
|
+
buf += dec.decode(value, { stream: true });
|
|
448
|
+
let sep: number;
|
|
449
|
+
while ((sep = buf.indexOf("\n\n")) !== -1) {
|
|
450
|
+
const frame = buf.slice(0, sep);
|
|
451
|
+
buf = buf.slice(sep + 2);
|
|
452
|
+
const data = frame
|
|
453
|
+
.split("\n")
|
|
454
|
+
.find((l) => l.startsWith("data: "))
|
|
455
|
+
?.slice(6);
|
|
456
|
+
if (data == null) continue;
|
|
457
|
+
if (data === "[DONE]" || data === '"[DONE]"') {
|
|
458
|
+
if (!finalized) {
|
|
459
|
+
finalized = true;
|
|
460
|
+
emit(translator.finalize());
|
|
461
|
+
}
|
|
462
|
+
continue;
|
|
463
|
+
}
|
|
464
|
+
const parsed = JSON.parse(data);
|
|
465
|
+
if (parsed?.error) {
|
|
466
|
+
emit([
|
|
467
|
+
sse("error", {
|
|
468
|
+
type: "error",
|
|
469
|
+
error: { type: "api_error", message: parsed.error.message ?? "generation failed" },
|
|
470
|
+
}),
|
|
471
|
+
]);
|
|
472
|
+
continue;
|
|
473
|
+
}
|
|
474
|
+
emit(translator.addChunk(parsed));
|
|
475
|
+
}
|
|
476
|
+
}
|
|
477
|
+
if (!finalized) emit(translator.finalize());
|
|
478
|
+
} catch (e) {
|
|
479
|
+
emit([
|
|
480
|
+
sse("error", {
|
|
481
|
+
type: "error",
|
|
482
|
+
error: { type: "api_error", message: (e as Error).message },
|
|
483
|
+
}),
|
|
484
|
+
]);
|
|
485
|
+
} finally {
|
|
486
|
+
reader.releaseLock();
|
|
487
|
+
controller.close();
|
|
488
|
+
}
|
|
489
|
+
},
|
|
490
|
+
});
|
|
491
|
+
}
|