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
|
@@ -0,0 +1,500 @@
|
|
|
1
|
+
// BFCL-V3 (Berkeley Function Calling Leaderboard) — "simple" category.
|
|
2
|
+
// Port of optiq/eval/bfcl.py. Single-turn function calling: from a user
|
|
3
|
+
// query + tool schema(s), generate the model's function call, parse it,
|
|
4
|
+
// and score AST-equivalence against the possible answer(s).
|
|
5
|
+
//
|
|
6
|
+
// Note on the prompt path: bfcl.py prefers tokenizer.apply_chat_template
|
|
7
|
+
// with a `tools=` kwarg; if the template rejects tools (or there is no
|
|
8
|
+
// chat template) it falls back to a textual prompt that embeds the tools
|
|
9
|
+
// JSON and asks for a <tool_call>...</tool_call> block. The mlx-bun eval
|
|
10
|
+
// harness exposes generation only through generateText(), which wraps the
|
|
11
|
+
// body as a single user turn and does NOT forward `tools` to the chat
|
|
12
|
+
// template. We therefore use bfcl.py's textual-fallback prompt verbatim,
|
|
13
|
+
// rendered through the chat template via useChat:true. This is a faithful
|
|
14
|
+
// reproduction of bfcl.py's fallback branch; the AST matcher — the heart
|
|
15
|
+
// of the task — is ported exactly. (See summary for this one note.)
|
|
16
|
+
|
|
17
|
+
import { generateText, loadJsonl, sampleIndices, type TaskModel } from "../runner";
|
|
18
|
+
|
|
19
|
+
// ---------------------------------------------------------------------------
|
|
20
|
+
// Data shapes (the merged jsonl: each line = {query, answer}).
|
|
21
|
+
// ---------------------------------------------------------------------------
|
|
22
|
+
|
|
23
|
+
interface ChatTurn { role: string; content: string }
|
|
24
|
+
|
|
25
|
+
interface ToolDef {
|
|
26
|
+
name?: string;
|
|
27
|
+
description?: string;
|
|
28
|
+
parameters?: unknown;
|
|
29
|
+
function?: unknown;
|
|
30
|
+
[k: string]: unknown;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
interface BfclQuery {
|
|
34
|
+
id: string;
|
|
35
|
+
question: ChatTurn[][] | ChatTurn[] | string;
|
|
36
|
+
function: ToolDef[] | string;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
interface BfclAnswer {
|
|
40
|
+
id: string;
|
|
41
|
+
ground_truth: Record<string, unknown>[] | Record<string, unknown> | string;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
interface BfclRow {
|
|
45
|
+
query: BfclQuery;
|
|
46
|
+
answer: BfclAnswer;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
export interface BfclResult {
|
|
50
|
+
nCorrect: number;
|
|
51
|
+
nTotal: number;
|
|
52
|
+
accuracy: number; // 0..1
|
|
53
|
+
nNoCall: number; // model didn't emit a parseable call
|
|
54
|
+
nWrongName: number; // right shape, wrong function name
|
|
55
|
+
nWrongArgs: number; // right name, wrong arguments
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
type ToolCall = { name: string; arguments: Record<string, unknown> };
|
|
59
|
+
|
|
60
|
+
// ---------------------------------------------------------------------------
|
|
61
|
+
// _extract_tool_call — recognize the four emit formats from bfcl.py.
|
|
62
|
+
// ---------------------------------------------------------------------------
|
|
63
|
+
|
|
64
|
+
/** Extract a single tool call from a model response, or null. */
|
|
65
|
+
export function extractToolCall(response: string): ToolCall | null {
|
|
66
|
+
// Strip thinking blocks if present.
|
|
67
|
+
let text = response;
|
|
68
|
+
if (text.includes("</think>")) text = text.split("</think>").slice(1).join("</think>");
|
|
69
|
+
if (text.includes("<channel|>")) text = text.split("<channel|>").slice(1).join("<channel|>");
|
|
70
|
+
|
|
71
|
+
// Format 1: <tool_call>...</tool_call> (Hermes / Qwen3 / Gemma-4 textual)
|
|
72
|
+
{
|
|
73
|
+
const m = text.match(/<tool_call>\s*([\s\S]+?)\s*<\/tool_call>/);
|
|
74
|
+
if (m) {
|
|
75
|
+
try {
|
|
76
|
+
const obj = JSON.parse(m[1]!) as Record<string, unknown>;
|
|
77
|
+
const call = toolCallFromJsonObject(obj);
|
|
78
|
+
if (call) return call;
|
|
79
|
+
} catch { /* fall through */ }
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
// Format 2: <function=NAME><parameter=K>V</parameter>...</function> (Qwen3.6)
|
|
84
|
+
{
|
|
85
|
+
const m = text.match(/<function=([^>\s]+)>([\s\S]*?)<\/function>/);
|
|
86
|
+
if (m) {
|
|
87
|
+
const name = m[1]!.trim();
|
|
88
|
+
const body = m[2]!;
|
|
89
|
+
const args: Record<string, unknown> = {};
|
|
90
|
+
const pre = /<parameter=([^>\s]+)>\s*([\s\S]*?)\s*<\/parameter>/g;
|
|
91
|
+
let pm: RegExpExecArray | null;
|
|
92
|
+
while ((pm = pre.exec(body)) !== null) {
|
|
93
|
+
args[pm[1]!.trim()] = pm[2]!.trim();
|
|
94
|
+
}
|
|
95
|
+
return { name, arguments: args };
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
// Format 3: Gemma-4 — <|tool_call>call:NAME{k:v,...}<tool_call|>
|
|
100
|
+
{
|
|
101
|
+
const m = text.match(/<\|tool_call>\s*call:(\S+?)\s*(?:\{([\s\S]*?)\})?\s*<tool_call\|>/);
|
|
102
|
+
if (m) {
|
|
103
|
+
const name = m[1]!.trim().replace(/\{+$/, "");
|
|
104
|
+
const body = (m[2] ?? "").split('<|"|>').join('"');
|
|
105
|
+
return { name, arguments: parseGemmaArgs(body) };
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
// Format 4: bare JSON {"name": ..., "arguments": ...} (or "function"/"args").
|
|
110
|
+
{
|
|
111
|
+
const re = /\{[^{]*"(?:name|function)"[^{]*"(?:arguments|args)"[^{]*\}/g;
|
|
112
|
+
let m: RegExpExecArray | null;
|
|
113
|
+
while ((m = re.exec(text)) !== null) {
|
|
114
|
+
try {
|
|
115
|
+
const obj = JSON.parse(m[0]) as Record<string, unknown>;
|
|
116
|
+
const call = toolCallFromJsonObject(obj);
|
|
117
|
+
if (call) return call;
|
|
118
|
+
} catch { /* continue */ }
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
return null;
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
/** Recognize a tool call inside a parsed JSON object, tolerating the key
|
|
126
|
+
* variants Gemma-4 emits under the textual prompt:
|
|
127
|
+
* {"name"|"function": <str>, "arguments"|"args"|"parameters": <obj|str>}
|
|
128
|
+
* Also accepts the BFCL-native single-entry mapping {funcName: {args}},
|
|
129
|
+
* where the lone key is the function name and its object value is the args. */
|
|
130
|
+
function toolCallFromJsonObject(obj: unknown): ToolCall | null {
|
|
131
|
+
if (!obj || typeof obj !== "object" || Array.isArray(obj)) return null;
|
|
132
|
+
const o = obj as Record<string, unknown>;
|
|
133
|
+
|
|
134
|
+
// Name under "name" or "function" (when "function" holds a string, not a
|
|
135
|
+
// nested {name, arguments} object).
|
|
136
|
+
let nameRaw: unknown = undefined;
|
|
137
|
+
if (typeof o.name === "string") nameRaw = o.name;
|
|
138
|
+
else if (typeof o.function === "string") nameRaw = o.function;
|
|
139
|
+
|
|
140
|
+
if (nameRaw !== undefined) {
|
|
141
|
+
let args = (o.arguments ?? o.args ?? o.parameters ?? {}) as unknown;
|
|
142
|
+
if (typeof args === "string") {
|
|
143
|
+
try { args = JSON.parse(args); } catch { /* leave as-is */ }
|
|
144
|
+
}
|
|
145
|
+
return {
|
|
146
|
+
name: String(nameRaw),
|
|
147
|
+
arguments: (args && typeof args === "object" && !Array.isArray(args)
|
|
148
|
+
? args
|
|
149
|
+
: {}) as Record<string, unknown>,
|
|
150
|
+
};
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
// OpenAI-nested: {"function": {"name": ..., "arguments": ...}}.
|
|
154
|
+
if (o.function && typeof o.function === "object" && !Array.isArray(o.function)) {
|
|
155
|
+
const inner = toolCallFromJsonObject(o.function);
|
|
156
|
+
if (inner) return inner;
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
// BFCL-native mapping: exactly one key whose value is an args object, and
|
|
160
|
+
// none of the reserved call keys are present.
|
|
161
|
+
const keys = Object.keys(o);
|
|
162
|
+
if (keys.length === 1) {
|
|
163
|
+
const k = keys[0]!;
|
|
164
|
+
const v = o[k];
|
|
165
|
+
if (v && typeof v === "object" && !Array.isArray(v)) {
|
|
166
|
+
return { name: k, arguments: v as Record<string, unknown> };
|
|
167
|
+
}
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
return null;
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
/** Parse Gemma-4's flat `key:value, key:value` argument body. Char-walk to
|
|
174
|
+
* respect string quotes and bracket nesting, then JSON-decode each value. */
|
|
175
|
+
function parseGemmaArgs(body: string): Record<string, unknown> {
|
|
176
|
+
const args: Record<string, unknown> = {};
|
|
177
|
+
let i = 0;
|
|
178
|
+
const n = body.length;
|
|
179
|
+
const isWs = (c: string): boolean => c === " " || c === "\t" || c === "\n";
|
|
180
|
+
while (i < n) {
|
|
181
|
+
// Skip whitespace + leading commas.
|
|
182
|
+
while (i < n && (body[i] === "," || isWs(body[i]!))) i++;
|
|
183
|
+
if (i >= n) break;
|
|
184
|
+
// Read key (up to colon).
|
|
185
|
+
const kStart = i;
|
|
186
|
+
while (i < n && body[i] !== ":") i++;
|
|
187
|
+
if (i >= n) break;
|
|
188
|
+
let key = body.slice(kStart, i).trim();
|
|
189
|
+
key = stripQuotes(key);
|
|
190
|
+
i++; // skip ':'
|
|
191
|
+
while (i < n && isWs(body[i]!)) i++;
|
|
192
|
+
// Read value — respect quotes and bracket nesting.
|
|
193
|
+
const vStart = i;
|
|
194
|
+
let depth = 0;
|
|
195
|
+
let inStr = false;
|
|
196
|
+
let strQ = "";
|
|
197
|
+
while (i < n) {
|
|
198
|
+
const c = body[i]!;
|
|
199
|
+
if (inStr) {
|
|
200
|
+
if (c === "\\" && i + 1 < n) { i += 2; continue; }
|
|
201
|
+
if (c === strQ) inStr = false;
|
|
202
|
+
i++;
|
|
203
|
+
continue;
|
|
204
|
+
}
|
|
205
|
+
if (c === '"' || c === "'") { inStr = true; strQ = c; i++; continue; }
|
|
206
|
+
if (c === "[" || c === "{") { depth++; i++; continue; }
|
|
207
|
+
if (c === "]" || c === "}") {
|
|
208
|
+
if (depth === 0) break;
|
|
209
|
+
depth--; i++; continue;
|
|
210
|
+
}
|
|
211
|
+
if (c === "," && depth === 0) break;
|
|
212
|
+
i++;
|
|
213
|
+
}
|
|
214
|
+
const vStr = body.slice(vStart, i).trim();
|
|
215
|
+
try {
|
|
216
|
+
args[key] = JSON.parse(vStr);
|
|
217
|
+
} catch {
|
|
218
|
+
args[key] = stripQuotes(vStr);
|
|
219
|
+
}
|
|
220
|
+
}
|
|
221
|
+
return args;
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
/** Mirror Python's `.strip('"').strip("'")` — strip leading/trailing of each
|
|
225
|
+
* quote char in turn (NOT a balanced-pair strip). */
|
|
226
|
+
function stripQuotes(s: string): string {
|
|
227
|
+
let r = s.replace(/^"+/, "").replace(/"+$/, "");
|
|
228
|
+
r = r.replace(/^'+/, "").replace(/'+$/, "");
|
|
229
|
+
return r;
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
// ---------------------------------------------------------------------------
|
|
233
|
+
// AST-equivalence matcher — the correctness core, ported from bfcl.py.
|
|
234
|
+
// ---------------------------------------------------------------------------
|
|
235
|
+
|
|
236
|
+
/** Normalize a value for comparison: int/float-coerce numeric strings, parse
|
|
237
|
+
* bool strings, JSON-decode array/object strings, recurse into containers. */
|
|
238
|
+
function canonicalizeValue(v: unknown): unknown {
|
|
239
|
+
if (typeof v === "string") {
|
|
240
|
+
const s = v.trim();
|
|
241
|
+
// Try numeric (matching Python int()/float() semantics, best-effort).
|
|
242
|
+
const num = pyNumeric(s);
|
|
243
|
+
if (num !== null) return num;
|
|
244
|
+
// Try bool.
|
|
245
|
+
const lower = s.toLowerCase();
|
|
246
|
+
if (lower === "true" || lower === "false") return lower === "true";
|
|
247
|
+
// Try JSON for arrays/objects.
|
|
248
|
+
if (s.startsWith("[") || s.startsWith("{")) {
|
|
249
|
+
try { return canonicalizeValue(JSON.parse(s)); } catch { /* keep string */ }
|
|
250
|
+
}
|
|
251
|
+
return s.trim();
|
|
252
|
+
}
|
|
253
|
+
if (Array.isArray(v)) return v.map(canonicalizeValue);
|
|
254
|
+
if (v && typeof v === "object") {
|
|
255
|
+
const out: Record<string, unknown> = {};
|
|
256
|
+
for (const [k, x] of Object.entries(v as Record<string, unknown>)) out[k] = canonicalizeValue(x);
|
|
257
|
+
return out;
|
|
258
|
+
}
|
|
259
|
+
return v;
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
/** Best-effort port of Python's int()/float() coercion used in bfcl.py's
|
|
263
|
+
* _canonicalize_value: if the string contains '.' or 'e', try float; else
|
|
264
|
+
* try int. Returns null if not a clean number (so callers keep the string). */
|
|
265
|
+
function pyNumeric(s: string): number | null {
|
|
266
|
+
if (s.length === 0) return null;
|
|
267
|
+
const hasDotOrE = s.includes(".") || s.toLowerCase().includes("e");
|
|
268
|
+
if (hasDotOrE) {
|
|
269
|
+
// Python float(): accepts leading/trailing space (already trimmed),
|
|
270
|
+
// optional sign, decimal, exponent. Reject anything else.
|
|
271
|
+
if (/^[+-]?(\d+\.?\d*|\.\d+)([eE][+-]?\d+)?$/.test(s) || /^[+-]?\d+[eE][+-]?\d+$/.test(s)) {
|
|
272
|
+
const f = Number(s);
|
|
273
|
+
return Number.isFinite(f) ? f : null;
|
|
274
|
+
}
|
|
275
|
+
return null;
|
|
276
|
+
}
|
|
277
|
+
// Python int(): plain optional-sign digit string (base 10).
|
|
278
|
+
if (/^[+-]?\d+$/.test(s)) {
|
|
279
|
+
const n = Number(s);
|
|
280
|
+
return Number.isFinite(n) ? n : null;
|
|
281
|
+
}
|
|
282
|
+
return null;
|
|
283
|
+
}
|
|
284
|
+
|
|
285
|
+
/** Equality with int/float tolerance and case-insensitive string compare. */
|
|
286
|
+
function looseEq(a: unknown, b: unknown): boolean {
|
|
287
|
+
if (typeof a === "string" && typeof b === "string") {
|
|
288
|
+
return a.trim().toLowerCase() === b.trim().toLowerCase();
|
|
289
|
+
}
|
|
290
|
+
const aNum = typeof a === "number" || typeof a === "boolean";
|
|
291
|
+
const bNum = typeof b === "number" || typeof b === "boolean";
|
|
292
|
+
// Python: bool is a subclass of int, so True == 1. Mirror numeric path for
|
|
293
|
+
// number/bool combos.
|
|
294
|
+
if ((typeof a === "number" || typeof a === "boolean") &&
|
|
295
|
+
(typeof b === "number" || typeof b === "boolean") &&
|
|
296
|
+
!(typeof a === "boolean" && typeof b === "boolean")) {
|
|
297
|
+
if (aNum && bNum) return Number(a) === Number(b);
|
|
298
|
+
}
|
|
299
|
+
if (typeof a === "number" && typeof b === "number") return a === b;
|
|
300
|
+
if (Array.isArray(a) && Array.isArray(b)) {
|
|
301
|
+
if (a.length !== b.length) return false;
|
|
302
|
+
return a.every((x, i) => looseEq(x, b[i]));
|
|
303
|
+
}
|
|
304
|
+
if (a && b && typeof a === "object" && typeof b === "object" && !Array.isArray(a) && !Array.isArray(b)) {
|
|
305
|
+
const ak = Object.keys(a as Record<string, unknown>);
|
|
306
|
+
const bk = Object.keys(b as Record<string, unknown>);
|
|
307
|
+
if (ak.length !== bk.length) return false;
|
|
308
|
+
const bset = new Set(bk);
|
|
309
|
+
if (!ak.every((k) => bset.has(k))) return false;
|
|
310
|
+
return ak.every((k) => looseEq((a as Record<string, unknown>)[k], (b as Record<string, unknown>)[k]));
|
|
311
|
+
}
|
|
312
|
+
return a === b;
|
|
313
|
+
}
|
|
314
|
+
|
|
315
|
+
/** Compare predicted call against ground truth. Ground truth may be a
|
|
316
|
+
* `{name: {arg: value}}` mapping (BFCL native) or a `{name, arguments}` dict. */
|
|
317
|
+
export function callsMatch(predicted: ToolCall | null, groundTruth: Record<string, unknown>): boolean {
|
|
318
|
+
if (!predicted) return false;
|
|
319
|
+
|
|
320
|
+
const predName = String(predicted.name ?? "").trim();
|
|
321
|
+
const predArgs = canonicalizeValue(predicted.arguments ?? {});
|
|
322
|
+
|
|
323
|
+
let gtName: string;
|
|
324
|
+
let gtArgsRaw: unknown;
|
|
325
|
+
if ("name" in groundTruth && "arguments" in groundTruth) {
|
|
326
|
+
gtName = String(groundTruth.name).trim();
|
|
327
|
+
gtArgsRaw = groundTruth.arguments ?? {};
|
|
328
|
+
} else {
|
|
329
|
+
const keys = Object.keys(groundTruth);
|
|
330
|
+
if (keys.length === 0) return false;
|
|
331
|
+
gtName = keys[0]!.trim();
|
|
332
|
+
gtArgsRaw = groundTruth[keys[0]!] ?? {};
|
|
333
|
+
}
|
|
334
|
+
const gtArgs = canonicalizeValue(gtArgsRaw);
|
|
335
|
+
|
|
336
|
+
if (predName !== gtName) return false;
|
|
337
|
+
if (!isPlainObject(predArgs) || !isPlainObject(gtArgs)) return false;
|
|
338
|
+
|
|
339
|
+
// Each ground-truth arg must be present and match. BFCL stores acceptable
|
|
340
|
+
// values as a list [primary, alt1, ...]; prediction may match ANY element.
|
|
341
|
+
// An arg is optional if its list includes '' (or null) — then a missing
|
|
342
|
+
// prediction is acceptable too.
|
|
343
|
+
for (const [k, gtV] of Object.entries(gtArgs)) {
|
|
344
|
+
const isList = Array.isArray(gtV);
|
|
345
|
+
const isOptional = isList && (gtV as unknown[]).some((x) => (typeof x === "string" && x === "") || x === null);
|
|
346
|
+
if (!(k in predArgs)) {
|
|
347
|
+
if (isOptional) continue;
|
|
348
|
+
return false;
|
|
349
|
+
}
|
|
350
|
+
const pv = predArgs[k];
|
|
351
|
+
if (isList && !Array.isArray(pv)) {
|
|
352
|
+
const ok = (gtV as unknown[]).some((gv) => looseEq(pv, gv));
|
|
353
|
+
if (!ok) return false;
|
|
354
|
+
} else {
|
|
355
|
+
if (!looseEq(pv, gtV)) return false;
|
|
356
|
+
}
|
|
357
|
+
}
|
|
358
|
+
return true;
|
|
359
|
+
}
|
|
360
|
+
|
|
361
|
+
function isPlainObject(v: unknown): v is Record<string, unknown> {
|
|
362
|
+
return !!v && typeof v === "object" && !Array.isArray(v);
|
|
363
|
+
}
|
|
364
|
+
|
|
365
|
+
// ---------------------------------------------------------------------------
|
|
366
|
+
// Prompt construction — bfcl.py's textual-fallback branch.
|
|
367
|
+
// ---------------------------------------------------------------------------
|
|
368
|
+
|
|
369
|
+
/** Wrap BFCL tool defs into the OpenAI/Anthropic chat-template tools schema. */
|
|
370
|
+
function wrapToolsForChat(raw: ToolDef[] | string | undefined): unknown[] {
|
|
371
|
+
let tools: ToolDef[];
|
|
372
|
+
if (typeof raw === "string") {
|
|
373
|
+
try { tools = JSON.parse(raw) as ToolDef[]; } catch { tools = []; }
|
|
374
|
+
} else {
|
|
375
|
+
tools = raw ?? [];
|
|
376
|
+
}
|
|
377
|
+
const out: unknown[] = [];
|
|
378
|
+
for (const t of tools) {
|
|
379
|
+
if (!t || typeof t !== "object") continue;
|
|
380
|
+
if ("function" in t) { out.push(t); continue; }
|
|
381
|
+
out.push({
|
|
382
|
+
type: "function",
|
|
383
|
+
function: {
|
|
384
|
+
name: t.name ?? "",
|
|
385
|
+
description: t.description ?? "",
|
|
386
|
+
parameters: t.parameters ?? { type: "object", properties: {} },
|
|
387
|
+
},
|
|
388
|
+
});
|
|
389
|
+
}
|
|
390
|
+
return out;
|
|
391
|
+
}
|
|
392
|
+
|
|
393
|
+
/** Extract the single user turn from BFCL's nested `question` shape. */
|
|
394
|
+
function userText(question: BfclQuery["question"]): string {
|
|
395
|
+
if (Array.isArray(question) && question.length > 0) {
|
|
396
|
+
const first = question[0];
|
|
397
|
+
if (Array.isArray(first)) {
|
|
398
|
+
// list-of-list: [[{role,content}, ...]]
|
|
399
|
+
return first.length > 0 ? (first[first.length - 1]!.content ?? "") : "";
|
|
400
|
+
}
|
|
401
|
+
if (first && typeof first === "object") {
|
|
402
|
+
// list-of-dict: [{role,content}, ...]
|
|
403
|
+
const turns = question as ChatTurn[];
|
|
404
|
+
return turns[turns.length - 1]!.content ?? "";
|
|
405
|
+
}
|
|
406
|
+
}
|
|
407
|
+
return question ? String(question) : "";
|
|
408
|
+
}
|
|
409
|
+
|
|
410
|
+
/** Build the textual prompt body (bfcl.py fallback branch). Rendered as a
|
|
411
|
+
* single user turn by generateText(useChat:true). */
|
|
412
|
+
function buildPrompt(text: string, tools: unknown[]): string {
|
|
413
|
+
const toolsJson = JSON.stringify(tools, null, 2);
|
|
414
|
+
return (
|
|
415
|
+
`Available tools:\n${toolsJson}\n\n` +
|
|
416
|
+
`User: ${text}\n\n` +
|
|
417
|
+
`To call a tool, respond with a single <tool_call>...</tool_call> block ` +
|
|
418
|
+
`containing one JSON object with exactly two keys: "name" (the tool name) ` +
|
|
419
|
+
`and "arguments" (an object of argument name -> value). ` +
|
|
420
|
+
`Use the exact key names "name" and "arguments". For example:\n` +
|
|
421
|
+
`<tool_call>{"name": "the_tool_name", "arguments": {"arg1": "value1", "arg2": 2}}</tool_call>\n`
|
|
422
|
+
);
|
|
423
|
+
}
|
|
424
|
+
|
|
425
|
+
// ---------------------------------------------------------------------------
|
|
426
|
+
// Ground-truth extraction.
|
|
427
|
+
// ---------------------------------------------------------------------------
|
|
428
|
+
|
|
429
|
+
function parseGroundTruth(raw: BfclAnswer["ground_truth"]): Record<string, unknown> {
|
|
430
|
+
let gt: unknown = raw;
|
|
431
|
+
if (typeof gt === "string") {
|
|
432
|
+
try { gt = JSON.parse(gt); } catch { /* leave */ }
|
|
433
|
+
}
|
|
434
|
+
if (Array.isArray(gt) && gt.length > 0) return (gt[0] ?? {}) as Record<string, unknown>;
|
|
435
|
+
if (gt && typeof gt === "object" && !Array.isArray(gt)) return gt as Record<string, unknown>;
|
|
436
|
+
return {};
|
|
437
|
+
}
|
|
438
|
+
|
|
439
|
+
function gtName(gt: Record<string, unknown>): string {
|
|
440
|
+
if ("name" in gt) return String(gt.name);
|
|
441
|
+
const keys = Object.keys(gt);
|
|
442
|
+
return keys.length ? keys[0]! : "";
|
|
443
|
+
}
|
|
444
|
+
|
|
445
|
+
// ---------------------------------------------------------------------------
|
|
446
|
+
// Eval loop.
|
|
447
|
+
// ---------------------------------------------------------------------------
|
|
448
|
+
|
|
449
|
+
export async function evaluateBfcl(
|
|
450
|
+
tm: TaskModel,
|
|
451
|
+
opts: { nSamples?: number; maxTokens?: number; seed?: number } = {},
|
|
452
|
+
): Promise<BfclResult> {
|
|
453
|
+
const maxTokens = opts.maxTokens ?? 512;
|
|
454
|
+
const rows = loadJsonl<BfclRow>("bfcl");
|
|
455
|
+
const idx = sampleIndices(rows.length, opts.nSamples ?? 200, opts.seed ?? 42);
|
|
456
|
+
|
|
457
|
+
let nCorrect = 0;
|
|
458
|
+
let nNoCall = 0;
|
|
459
|
+
let nWrongName = 0;
|
|
460
|
+
let nWrongArgs = 0;
|
|
461
|
+
|
|
462
|
+
for (let k = 0; k < idx.length; k++) {
|
|
463
|
+
const row = rows[idx[k]!]!;
|
|
464
|
+
const q = row.query;
|
|
465
|
+
const text = userText(q.question);
|
|
466
|
+
const tools = wrapToolsForChat(q.function);
|
|
467
|
+
const prompt = buildPrompt(text, tools);
|
|
468
|
+
|
|
469
|
+
const out = await generateText(tm, prompt, { maxTokens, useChat: true });
|
|
470
|
+
const predicted = extractToolCall(out);
|
|
471
|
+
const gt = parseGroundTruth(row.answer.ground_truth);
|
|
472
|
+
|
|
473
|
+
if (predicted === null) {
|
|
474
|
+
nNoCall++;
|
|
475
|
+
} else if (!callsMatch(predicted, gt)) {
|
|
476
|
+
const predName = String(predicted.name ?? "").trim();
|
|
477
|
+
if (predName !== gtName(gt).trim()) nWrongName++;
|
|
478
|
+
else nWrongArgs++;
|
|
479
|
+
} else {
|
|
480
|
+
nCorrect++;
|
|
481
|
+
}
|
|
482
|
+
|
|
483
|
+
if ((k + 1) % 10 === 0 || k + 1 === idx.length) {
|
|
484
|
+
process.stderr.write(
|
|
485
|
+
`\r bfcl ${k + 1}/${idx.length} acc=${((nCorrect / (k + 1)) * 100).toFixed(1)}%`,
|
|
486
|
+
);
|
|
487
|
+
}
|
|
488
|
+
}
|
|
489
|
+
process.stderr.write("\n");
|
|
490
|
+
|
|
491
|
+
const nTotal = idx.length;
|
|
492
|
+
return {
|
|
493
|
+
nCorrect,
|
|
494
|
+
nTotal,
|
|
495
|
+
accuracy: nTotal ? nCorrect / nTotal : 0,
|
|
496
|
+
nNoCall,
|
|
497
|
+
nWrongName,
|
|
498
|
+
nWrongArgs,
|
|
499
|
+
};
|
|
500
|
+
}
|
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
// GSM8K — grade-school math accuracy. Port of optiq/eval/gsm8k.py:
|
|
2
|
+
// 3-shot CoT, greedy decode, extract the numeric answer, compare.
|
|
3
|
+
|
|
4
|
+
import { generateText, loadJsonl, sampleIndices, type TaskModel } from "../runner";
|
|
5
|
+
|
|
6
|
+
// 3-shot exemplars (verbatim from gsm8k.py).
|
|
7
|
+
const FEW_SHOT: { question: string; answer: string }[] = [
|
|
8
|
+
{
|
|
9
|
+
question: "There are 15 trees in the grove. Grove workers will plant trees in the grove today. After they are done, there will be 21 trees. How many trees did the grove workers plant today?",
|
|
10
|
+
answer: "There are 15 trees originally. Then there were 21 trees after some more were planted. So there must have been 21 - 15 = 6 trees planted.\n#### 6",
|
|
11
|
+
},
|
|
12
|
+
{
|
|
13
|
+
question: "If there are 3 cars in the parking lot and 2 more cars arrive, how many cars are in the parking lot?",
|
|
14
|
+
answer: "There are originally 3 cars. 2 more cars arrive. 3 + 2 = 5.\n#### 5",
|
|
15
|
+
},
|
|
16
|
+
{
|
|
17
|
+
question: "Leah had 32 chocolates and her sister had 42. If they ate 35, how many pieces do they have left in total?",
|
|
18
|
+
answer: "Originally, Leah had 32 chocolates. Her sister had 42. So in total they had 32 + 42 = 74. After eating 35, they had 74 - 35 = 39.\n#### 39",
|
|
19
|
+
},
|
|
20
|
+
];
|
|
21
|
+
|
|
22
|
+
interface Gsm8kRow { question: string; answer: string }
|
|
23
|
+
|
|
24
|
+
export interface Gsm8kResult {
|
|
25
|
+
nCorrect: number;
|
|
26
|
+
nTotal: number;
|
|
27
|
+
accuracy: number; // 0..1
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
function buildPrompt(question: string, nShots: number): string {
|
|
31
|
+
let p = "";
|
|
32
|
+
for (const ex of FEW_SHOT.slice(0, nShots)) p += `Q: ${ex.question}\nA: ${ex.answer}\n\n`;
|
|
33
|
+
return p + `Q: ${question}\nA:`;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
const NUM = String.raw`-?[\d,]+\.?\d*`;
|
|
37
|
+
|
|
38
|
+
/** Model output → numeric answer string (####, then \boxed{}, then last number). */
|
|
39
|
+
export function extractAnswer(textIn: string): string | null {
|
|
40
|
+
let text = textIn;
|
|
41
|
+
if (text.includes("</think>")) text = text.split("</think>")[1] ?? text;
|
|
42
|
+
|
|
43
|
+
const hash = text.match(new RegExp(String.raw`####\s*(${NUM})`));
|
|
44
|
+
if (hash) return hash[1]!.replace(/,/g, "").trim();
|
|
45
|
+
|
|
46
|
+
const boxed = text.match(new RegExp(String.raw`\\boxed\{\s*(${NUM})\s*\}`));
|
|
47
|
+
if (boxed) return boxed[1]!.replace(/,/g, "").trim();
|
|
48
|
+
|
|
49
|
+
const nums = text.match(new RegExp(NUM, "g"));
|
|
50
|
+
return nums && nums.length ? nums[nums.length - 1]!.replace(/,/g, "").trim() : null;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
function groundTruth(answer: string): string {
|
|
54
|
+
const m = answer.match(new RegExp(String.raw`####\s*(${NUM})`));
|
|
55
|
+
return m ? m[1]!.replace(/,/g, "").trim() : "";
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
function toNum(s: string | null): number | null {
|
|
59
|
+
if (!s) return null;
|
|
60
|
+
const v = Number(s.replace(/,/g, ""));
|
|
61
|
+
return Number.isFinite(v) ? v : null;
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
export async function evaluateGsm8k(
|
|
65
|
+
tm: TaskModel,
|
|
66
|
+
opts: { nSamples?: number; nShots?: number; maxTokens?: number; seed?: number } = {},
|
|
67
|
+
): Promise<Gsm8kResult> {
|
|
68
|
+
const nShots = opts.nShots ?? 3;
|
|
69
|
+
const maxTokens = opts.maxTokens ?? 256;
|
|
70
|
+
const rows = loadJsonl<Gsm8kRow>("gsm8k");
|
|
71
|
+
const idx = sampleIndices(rows.length, opts.nSamples ?? 200, opts.seed ?? 42);
|
|
72
|
+
|
|
73
|
+
let nCorrect = 0;
|
|
74
|
+
for (let k = 0; k < idx.length; k++) {
|
|
75
|
+
const item = rows[idx[k]!]!;
|
|
76
|
+
const gt = toNum(groundTruth(item.answer));
|
|
77
|
+
const out = await generateText(tm, buildPrompt(item.question, nShots), { maxTokens, useChat: true });
|
|
78
|
+
const pred = toNum(extractAnswer(out));
|
|
79
|
+
if (gt !== null && pred !== null && Math.abs(gt - pred) < 1e-3) nCorrect++;
|
|
80
|
+
if ((k + 1) % 10 === 0 || k + 1 === idx.length)
|
|
81
|
+
process.stderr.write(`\r gsm8k ${k + 1}/${idx.length} acc=${(nCorrect / (k + 1) * 100).toFixed(1)}%`);
|
|
82
|
+
}
|
|
83
|
+
process.stderr.write("\n");
|
|
84
|
+
return { nCorrect, nTotal: idx.length, accuracy: idx.length ? nCorrect / idx.length : 0 };
|
|
85
|
+
}
|
|
@@ -0,0 +1,109 @@
|
|
|
1
|
+
// HashHop — long-context multi-hop key/value retrieval. Port of
|
|
2
|
+
// optiq/eval/hashhop.py. The generator (Magic AI's HashHop, codelion's
|
|
3
|
+
// `hashhop` package) isn't on PyPI, so we reconstruct MultiHopEval.make_one
|
|
4
|
+
// from the documented format: a chain h0=h1, h1=h2, …, h(N-1)='hN' (quoted
|
|
5
|
+
// terminal) buried in many distractor assignments, serialized as a shuffled
|
|
6
|
+
// dictionary. The model walks N lookups from h0 and reports hN.
|
|
7
|
+
//
|
|
8
|
+
// This is the long-context guard (≈12k-char dict → ~8–9k tokens): online-
|
|
9
|
+
// softmax / mixed-4/8-bit-KV error compounds across hops, so it's where the
|
|
10
|
+
// fused-attention kernel (opportunity A) has the most to prove.
|
|
11
|
+
|
|
12
|
+
import { generateText, type TaskModel } from "../runner";
|
|
13
|
+
|
|
14
|
+
const INSTR =
|
|
15
|
+
"You will be given a list of hash assignments and a starting hash. " +
|
|
16
|
+
"Walk the chain by repeated key->value lookup until you hit a terminal " +
|
|
17
|
+
"value (written with single quotes, e.g. `KEY = 'VALUE'`). Output ONLY " +
|
|
18
|
+
"the final 16-character hash inside the quotes, with no extra words or " +
|
|
19
|
+
"punctuation.";
|
|
20
|
+
|
|
21
|
+
const ALPHABET = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz";
|
|
22
|
+
const HASH_LEN = 16;
|
|
23
|
+
|
|
24
|
+
function mulberry32(seed: number): () => number {
|
|
25
|
+
let s = seed >>> 0;
|
|
26
|
+
return () => {
|
|
27
|
+
s = (s + 0x6d2b79f5) | 0;
|
|
28
|
+
let t = Math.imul(s ^ (s >>> 15), 1 | s);
|
|
29
|
+
t = (t + Math.imul(t ^ (t >>> 7), 61 | t)) ^ t;
|
|
30
|
+
return ((t ^ (t >>> 14)) >>> 0) / 4294967296;
|
|
31
|
+
};
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
function randHash(rand: () => number): string {
|
|
35
|
+
let h = "";
|
|
36
|
+
for (let i = 0; i < HASH_LEN; i++) h += ALPHABET[Math.floor(rand() * ALPHABET.length)];
|
|
37
|
+
return h;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
interface Sample { prompt: string; start: string; expected: string }
|
|
41
|
+
|
|
42
|
+
/** One multi-hop instance: a length-`hops` chain padded with distractors to
|
|
43
|
+
* ~`nChars`, lines shuffled. start = h0, expected = h(hops). */
|
|
44
|
+
function makeOne(rand: () => number, hops: number, nChars: number): Sample {
|
|
45
|
+
const chain: string[] = [];
|
|
46
|
+
for (let i = 0; i <= hops; i++) chain.push(randHash(rand));
|
|
47
|
+
const lines: string[] = [];
|
|
48
|
+
for (let i = 0; i < hops; i++)
|
|
49
|
+
lines.push(i === hops - 1 ? `${chain[i]} = '${chain[i + 1]}'` : `${chain[i]} = ${chain[i + 1]}`);
|
|
50
|
+
|
|
51
|
+
let chars = lines.reduce((a, l) => a + l.length + 1, 0);
|
|
52
|
+
while (chars < nChars) {
|
|
53
|
+
const a = randHash(rand);
|
|
54
|
+
const b = randHash(rand);
|
|
55
|
+
const line = rand() < 0.5 ? `${a} = '${b}'` : `${a} = ${b}`;
|
|
56
|
+
lines.push(line);
|
|
57
|
+
chars += line.length + 1;
|
|
58
|
+
}
|
|
59
|
+
for (let i = lines.length - 1; i > 0; i--) {
|
|
60
|
+
const j = Math.floor(rand() * (i + 1));
|
|
61
|
+
[lines[i], lines[j]] = [lines[j]!, lines[i]!];
|
|
62
|
+
}
|
|
63
|
+
return { prompt: lines.join("\n"), start: chain[0]!, expected: chain[hops]! };
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
function buildPrompt(s: Sample): string {
|
|
67
|
+
return `${INSTR}\n\nDictionary:\n${s.prompt}\n\nStarting hash: ${s.start}\nFinal hash:`;
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
/** First 16-char alphabetic run in the output (port of _extract_pred). */
|
|
71
|
+
function extractPred(out: string): string {
|
|
72
|
+
const m = out.match(/[A-Za-z]{16}/);
|
|
73
|
+
return m ? m[0] : out.trim();
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
export interface HashhopResult {
|
|
77
|
+
nTotal: number;
|
|
78
|
+
nCorrect: number;
|
|
79
|
+
accuracy: number; // overall, 0..1
|
|
80
|
+
byHops: Record<number, number>;
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
export async function evaluateHashhop(
|
|
84
|
+
tm: TaskModel,
|
|
85
|
+
opts: { nPerHop?: number; hopsList?: number[]; chars?: number; maxTokens?: number; seed?: number } = {},
|
|
86
|
+
): Promise<HashhopResult> {
|
|
87
|
+
const nPerHop = opts.nPerHop ?? 25;
|
|
88
|
+
const hopsList = opts.hopsList ?? [1, 2, 3, 4];
|
|
89
|
+
const chars = opts.chars ?? 12000;
|
|
90
|
+
const maxTokens = opts.maxTokens ?? 32;
|
|
91
|
+
const rand = mulberry32(opts.seed ?? 42);
|
|
92
|
+
|
|
93
|
+
const byHops: Record<number, number> = {};
|
|
94
|
+
let total = 0;
|
|
95
|
+
let correct = 0;
|
|
96
|
+
for (const hops of hopsList) {
|
|
97
|
+
let hc = 0;
|
|
98
|
+
for (let k = 0; k < nPerHop; k++) {
|
|
99
|
+
const s = makeOne(rand, hops, chars);
|
|
100
|
+
const out = await generateText(tm, buildPrompt(s), { maxTokens, useChat: true });
|
|
101
|
+
if (extractPred(out) === s.expected) { hc++; correct++; }
|
|
102
|
+
total++;
|
|
103
|
+
process.stderr.write(`\r hashhop hops=${hops} ${k + 1}/${nPerHop} acc=${(hc / (k + 1) * 100).toFixed(0)}%`);
|
|
104
|
+
}
|
|
105
|
+
byHops[hops] = nPerHop ? hc / nPerHop : 0;
|
|
106
|
+
process.stderr.write("\n");
|
|
107
|
+
}
|
|
108
|
+
return { nTotal: total, nCorrect: correct, accuracy: total ? correct / total : 0, byHops };
|
|
109
|
+
}
|