lynkr 9.9.0 → 9.9.1

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/README.md CHANGED
@@ -246,9 +246,9 @@ POLICY_MAX_STEPS=50
246
246
  POLICY_MAX_TOOL_CALLS=100
247
247
  ```
248
248
 
249
- Lynkr analyzes each request and routes it to the appropriate tier. Simple questions use fast models. Complex refactoring uses powerful models.
249
+ Lynkr analyzes each request and routes it to the appropriate tier. Simple questions use fast models. Complex refactoring uses powerful models. The scorer combines anchor-embedding classification (WS7, payload-invariant) with an LLM difficulty classifier (Phase 6, added 2026-07-19) that catches topic-vs-difficulty confounding — `list the exports from this file` correctly routes to MEDIUM instead of getting boosted to REASONING by "technical vocabulary" alone.
250
250
 
251
- **Result:** 70-90% of requests use cheaper/faster models. Only hard problems hit expensive models.
251
+ **Result:** 70-90% of requests use cheaper/faster models. Only hard problems hit expensive models. **15× reduction in expensive-tier over-routing** vs the anchor-only baseline on the 381-prompt eval set (0.6% vs ~15%).
252
252
 
253
253
  Tier configuration is strictly authoritative — bandit exploration is constrained to the models you've listed in `TIER_*`, and multi-turn conversations score with a recency-weighted sliding window so context isn't lost on short follow-ups. Conversations get a content-fingerprint session id (clients like Claude Code send none), the decision pins for the session, and a guarded escape ladder (risk keywords, force phrases, score drift, context overflow) re-escalates the moment a task outgrows its model. Full pipeline: [`docs/routing-intelligence.md`](docs/routing-intelligence.md) · intent scorer: [`docs/intent-window-routing.md`](docs/intent-window-routing.md) · verify any change: [`docs/benchmarking.md`](docs/benchmarking.md).
254
254
 
@@ -413,7 +413,7 @@ npm start
413
413
 
414
414
  Head-to-head against **LiteLLM** on the **same backends** (Ollama `minimax-m2.5`, Moonshot, Azure OpenAI), 9 scenarios across 4 feature categories. Apples-to-apples comparison is Lynkr vs LiteLLM **billed tokens on the same scenario**. Run with `node benchmark-tier-routing.js`.
415
415
 
416
- > _Run: June 5, 2026 · Lynkr v9.3.2 · LiteLLM v1.87.1 · macOS, Apple Silicon._
416
+ > _Runs: token benchmarks — June 5, 2026 (Lynkr v9.3.2 · LiteLLM v1.87.1). Tier routing head-to-head — **re-run July 19, 2026** (Lynkr working tree w/ Phase-6 classifier + config B · LiteLLM v1.94.0.dev1 Auto Router v2). macOS, Apple Silicon._
417
417
 
418
418
  ### Token reduction (vs LiteLLM, same model & prompt)
419
419
 
@@ -433,28 +433,56 @@ Lynkr strips irrelevant tool schemas (smart tool selection) and binary-compresse
433
433
 
434
434
  Near-identical prompts return cached responses in 171ms. Zero model tokens billed on a cache hit.
435
435
 
436
- ### Tier routing — vs LiteLLM Auto Router v2 (July 15, 2026)
436
+ ### Tier routing — vs LiteLLM Auto Router v2 (re-run July 19, 2026)
437
437
 
438
- LiteLLM v1.94 shipped a native complexity router (`auto_router/complexity_router`) with the same four tier names Lynkr uses. Head-to-head on the **same backends** with identical prompts, both proxies judged against the same acceptable-tier sets (11 routing scenarios, `MODE=routing node benchmark-tier-routing.js`, LiteLLM config in `litellm-autorouter-v2.yaml`):
438
+ LiteLLM v1.94 shipped a native complexity router (`auto_router/complexity_router`) with the same four tier names Lynkr uses. Head-to-head on the **same backends** with identical prompts, both proxies live, both judged against the same acceptable-tier sets (11 routing scenarios, `MODE=routing node benchmark-tier-routing.js`, LiteLLM config in `litellm-autorouter-v2.yaml`):
439
439
 
440
- | Router | Routing-correct | Routing overhead |
440
+ | Router | Routing-correct | Notes |
441
441
  |---|---|---|
442
- | **Lynkr** | **11/11** | local embedding, ~0 marginal cost/latency after cache |
443
- | LiteLLM v2 — heuristic (default) | 4/11 | <1ms, free — but every miss under-routed hard work (banking security analysis, autonomous agentic loop) to the free local model |
444
- | LiteLLM v2 — LLM classifier | 6–8/11 (non-deterministic across runs) | a paid GPT-5.2 call + ~2–3s on **every** request; fails outright with local classifier models (structured-output errors → silent heuristic fallback) |
442
+ | **Lynkr (Phase-6 classifier + config B)** | **11/11 ✅** | anchor embedding + local LLM classifier reconcile + FORCE_REASONING patterns + risk-remap; ~500ms warm classifier call, cached |
443
+ | LiteLLM v2 — heuristic (default) | **4/11** | every miss *under-routed* banking security analysis, whole-pipeline refactor, prod auth-file fix, autonomous agentic loop all sent to a 7B local model |
444
+ | LiteLLM v2 — LLM classifier | 6–8/11 (non-deterministic) | paid GPT-5.2 call + ~2–3s on **every** request; fails outright with local classifier models |
445
445
 
446
- Lynkr scores cleaned user-authored text against embedding anchors plus 13 weighted heuristic dimensions, detects agentic workflows, and strips harness-injected noise before scoring. LiteLLM's router reads the raw last message and has no verify-then-escalate cascade — its fallbacks trigger only on HTTP errors, never on a bad answer.
446
+ **The 7 requests LiteLLM v2 misroutes to a 7B ollama model (should be COMPLEX or REASONING):**
447
447
 
448
- _Fairness notes: the 11 scenarios derive from Lynkr's own regression suite, so Lynkr has home-field advantage — the transferable finding is the direction of LiteLLM's failures (systematic under-routing on defaults; per-request cost and instability with the LLM classifier), not the exact scores. Lynkr's top tiers used Azure gpt-5.2-chat / Z.ai GLM-5.2; LiteLLM was given the identical tier targets._
448
+ | What you typed | Lynkr sends to | LiteLLM sends to |
449
+ |---|---|---|
450
+ | "Analyse security trade-offs of JWT vs httpOnly cookies for a banking app" | Claude Opus (top) | **ollama 7B** ❌ |
451
+ | "Refactor the entire ingestion pipeline and give me the plan" | z.ai GLM-5.2 (mid) | **ollama 7B** ❌ |
452
+ | "Fix the null-check bug in `src/auth/middleware.ts`" | Claude Opus (risk-remap) | **ollama 7B** ❌ |
453
+ | "Figure out why this test is flaky. You have full autonomy — iterate until 10 runs pass" | Claude Opus (autonomous) | **ollama 7B** ❌ |
454
+ | "please summarize the exports of this file quickly" | ollama minimax (correct — MEDIUM) | ollama minimax — but for the wrong reason (LiteLLM under-routed to SIMPLE) |
455
+
456
+ Lynkr layers the following, in decreasing priority, to catch every miss LiteLLM leaks:
457
+
458
+ - FORCE_REASONING regex (`ultrathink`, `prove`, `security audit`, `from first principles`) → deterministic top-tier
459
+ - Risk classifier (auth/middleware/credentials paths) → REASONING under config B
460
+ - Agentic detector (`AUTONOMOUS` workflows) → REASONING minimum
461
+ - Anchor-embedding classifier over 4 difficulty classes + FRONTIER_MIN_SIM floor
462
+ - Local LLM classifier (qwen2.5:3b) reconciles borderline anchor calls
463
+ - Envelope invariance — scores cleaned user text only, ignoring tool schemas / history / system-reminders
464
+
465
+ LiteLLM's router reads the raw last message with no envelope-stripping, no verify-then-escalate cascade, no risk classifier, no agentic detection — its fallbacks trigger only on HTTP errors, never on a bad answer.
466
+
467
+ _Fairness notes: the 11 scenarios derive from Lynkr's own regression suite, so Lynkr has home-field advantage — the transferable finding is the **direction** of LiteLLM's failures (systematic under-routing to cheap on hard prompts), not the exact scores. Lynkr's top tiers used Claude Opus 4.8 / z.ai GLM-5.2; LiteLLM was given the identical tier targets._
449
468
 
450
469
  ### Cost projection (100,000 requests/month, same backend)
451
470
 
471
+ **Direct cost (raw meter):**
472
+
452
473
  | | Monthly cost | vs LiteLLM |
453
474
  |---|---|---|
454
- | LiteLLM | ~$818 | baseline |
455
- | **Lynkr** | **~$409** | **~50% cheaper** |
475
+ | LiteLLM (TOON tool-heavy scenario) | ~$818 | baseline |
476
+ | **Lynkr (TOON tool-heavy)** | **~$409** | **~50% cheaper** via token optimization |
477
+
478
+ **Effective cost per correct-tier answer** (July 19 head-to-head, all 11 routing scenarios):
479
+
480
+ | | Direct $/mo | Effective $/mo if you re-issue misroutes at correct tier |
481
+ |---|---|---|
482
+ | **Lynkr** | ~$5.50 (spends where it should, doesn't where it shouldn't) | ~$5.50 |
483
+ | LiteLLM v2 heuristic | $0 (under-routes everything to ollama) | ~$89 (7 misroutes need re-issue on Moonshot/COMPLEX) |
456
484
 
457
- _Based on a tool-heavy agentic session (TOON scenario). On equal footing same provider, same modelLynkr is cheaper due to token optimization._
485
+ The story isn't "Lynkr always spends less." It's **Lynkr spends where it should and doesn't where it shouldn't**. LiteLLM's zero-dollar cost on complex prompts is bought with wrong-tier answers to hard worka 7B model answering "prove this rate limiter is fair under concurrent refill" is silent quality collapse, not a savings.
458
486
 
459
487
  → [Full benchmark report with methodology](BENCHMARK_REPORT.md)
460
488
 
package/bin/cli.js CHANGED
@@ -14,21 +14,23 @@ const SUBCOMMANDS = {
14
14
  };
15
15
 
16
16
  const sub = process.argv[2];
17
- if (sub && Object.prototype.hasOwnProperty.call(SUBCOMMANDS, sub)) {
17
+ // `lynkr start` is an alias for `lynkr` (start the proxy). Drop the token
18
+ // and fall through so users can use whichever spelling they prefer.
19
+ if (sub === 'start') {
20
+ process.argv.splice(2, 1);
21
+ } else if (sub && Object.prototype.hasOwnProperty.call(SUBCOMMANDS, sub)) {
18
22
  process.argv.splice(2, 1); // drop the subcommand token so the script's own arg parser is happy
19
23
  // Subcommand scripts check this to decide whether to invoke their main()
20
24
  // when they're require()'d (vs being loaded by a test for unit-checking).
21
25
  process.env._LYNKR_SUBCMD = sub;
22
26
  require(SUBCOMMANDS[sub]);
23
27
  return;
24
- }
25
-
26
- // Unknown positional commands must error loudly, not silently boot the
27
- // server (`lynkr codex` used to start a bare gateway and never say why).
28
- if (sub && !sub.startsWith('-')) {
28
+ } else if (sub && !sub.startsWith('-')) {
29
+ // Unknown positional commands must error loudly, not silently boot the
30
+ // server (`lynkr codex` used to start a bare gateway and never say why).
29
31
  console.error(`Error: unknown command '${sub}'.`);
30
32
  console.error(`Did you mean: lynkr wrap ${sub}`);
31
- console.error(`Known commands: ${Object.keys(SUBCOMMANDS).join(', ')} (or no command to start the server)`);
33
+ console.error(`Known commands: ${Object.keys(SUBCOMMANDS).join(', ')}, start (or no command to start the server)`);
32
34
  process.exit(1);
33
35
  }
34
36
 
@@ -45,6 +47,8 @@ ${pkg.description}
45
47
 
46
48
  Usage:
47
49
  lynkr [options] Start the proxy server (default)
50
+ lynkr start [options] Alias for the above
51
+ lynkr init [options] Interactive setup wizard (writes .env, pulls classifier model)
48
52
  lynkr wrap <target> [options] Wrap CLI tools through Lynkr proxy
49
53
  lynkr usage [options] Show AI spend report and tier-routing savings
50
54
  lynkr stats [options] Shareable savings-receipt card (also: lynkr usage --card)
package/bin/lynkr-init.js CHANGED
@@ -550,6 +550,36 @@ async function runInteractive(opts) {
550
550
  close();
551
551
  console.log('');
552
552
  writeEnvFile(buildEnvContent(env, isWrap, tierConfig), opts);
553
+
554
+ // ── Classifier bootstrap: detect ollama, pull model, warm-up ──
555
+ // Runs AFTER .env is written so the classifier can read tier config
556
+ // (currently uses hardcoded qwen2.5:3b, but leaves room for the future
557
+ // fine-tuned model to plug in via SIMPLE tier or an env override).
558
+ if (!opts.dryRun) {
559
+ console.log('');
560
+ console.log('Setting up the difficulty classifier…');
561
+ try {
562
+ const { ensureClassifierReady } = require('../src/routing/classifier-setup');
563
+ const rl = readline.createInterface({ input: process.stdin, output: process.stdout });
564
+ const prompt = (q) => new Promise((res) => rl.question(q, (a) => res(a)));
565
+ const result = await ensureClassifierReady({
566
+ mode: 'interactive',
567
+ log: (m) => console.log(m),
568
+ warn: (m) => console.warn(m),
569
+ prompt,
570
+ });
571
+ rl.close();
572
+ if (result.ready) {
573
+ console.log('✓ Classifier ready.');
574
+ } else if (result.reason === 'ollama_missing') {
575
+ console.log('(Install Ollama and re-run `lynkr init` to enable the classifier — Lynkr will still start without it, using anchor-only scoring.)');
576
+ } else {
577
+ console.log(`(Classifier disabled: ${result.reason}. Lynkr will fall back to anchor-only scoring.)`);
578
+ }
579
+ } catch (err) {
580
+ console.warn(`⚠ Classifier setup skipped: ${err.message}`);
581
+ }
582
+ }
553
583
  } catch (err) {
554
584
  close();
555
585
  throw err;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "lynkr",
3
- "version": "9.9.0",
3
+ "version": "9.9.1",
4
4
  "files": [
5
5
  "index.js",
6
6
  "install.sh",
@@ -36,7 +36,7 @@
36
36
  "dev": "nodemon index.js",
37
37
  "lint": "eslint src index.js",
38
38
  "test": "npm run test:unit && npm run test:performance",
39
- "test:unit": "DATABRICKS_API_KEY=test-key DATABRICKS_API_BASE=http://test.com LOG_FILE_ENABLED=false node --test test/routing.test.js test/hybrid-routing-integration.test.js test/web-tools.test.js test/passthrough-mode.test.js test/openrouter-error-resilience.test.js test/format-conversion.test.js test/azure-openai-config.test.js test/azure-openai-format-conversion.test.js test/azure-openai-routing.test.js test/azure-openai-streaming.test.js test/azure-openai-error-resilience.test.js test/azure-openai-integration.test.js test/openai-integration.test.js test/toon-compression.test.js test/gcf-compression.test.js test/llamacpp-integration.test.js test/resilience.test.js test/telemetry-routing.test.js test/memory/store.test.js test/memory/surprise.test.js test/memory/extractor.test.js test/memory/search.test.js test/memory/retriever.test.js test/distill.test.js test/large-payload.test.js test/code-mode.test.js test/prompt-cache-injection.test.js test/risk-analyzer.test.js test/interaction-block.test.js test/preflight.test.js test/token-reduction.test.js test/session-affinity.test.js test/model-registry-cost.test.js test/task-decomposition.test.js test/output-format-guard.test.js test/tier-fallback.test.js test/wrap.test.js test/init.test.js test/agent-learning.test.js test/tool-call-response-metadata.test.js test/degradation.test.js test/routing-telemetry-columns.test.js test/sticky-routing.test.js test/knn-ambiguous-escalate.test.js test/deescalator.test.js test/client-profiles.test.js test/strip-internal-fields.test.js test/complexity-tool-subtraction.test.js test/bandit.test.js test/routing-propensity.test.js test/reward-pipeline.test.js test/knn-cold-start.test.js test/calibration.test.js test/feedback-loop.test.js test/session-fingerprint.test.js test/side-request-guards.test.js test/verifier.test.js test/intent-score.test.js test/usage-stats.test.js test/loop-guard.test.js",
39
+ "test:unit": "DATABRICKS_API_KEY=test-key DATABRICKS_API_BASE=http://test.com LOG_FILE_ENABLED=false node --test test/routing.test.js test/hybrid-routing-integration.test.js test/web-tools.test.js test/passthrough-mode.test.js test/openrouter-error-resilience.test.js test/format-conversion.test.js test/azure-openai-config.test.js test/azure-openai-format-conversion.test.js test/azure-openai-routing.test.js test/azure-openai-streaming.test.js test/azure-openai-error-resilience.test.js test/azure-openai-integration.test.js test/openai-integration.test.js test/toon-compression.test.js test/gcf-compression.test.js test/llamacpp-integration.test.js test/resilience.test.js test/telemetry-routing.test.js test/memory/store.test.js test/memory/surprise.test.js test/memory/extractor.test.js test/memory/search.test.js test/memory/retriever.test.js test/distill.test.js test/large-payload.test.js test/code-mode.test.js test/prompt-cache-injection.test.js test/risk-analyzer.test.js test/interaction-block.test.js test/preflight.test.js test/token-reduction.test.js test/session-affinity.test.js test/model-registry-cost.test.js test/task-decomposition.test.js test/output-format-guard.test.js test/tier-fallback.test.js test/wrap.test.js test/init.test.js test/agent-learning.test.js test/tool-call-response-metadata.test.js test/degradation.test.js test/routing-telemetry-columns.test.js test/sticky-routing.test.js test/knn-ambiguous-escalate.test.js test/deescalator.test.js test/client-profiles.test.js test/strip-internal-fields.test.js test/complexity-tool-subtraction.test.js test/bandit.test.js test/routing-propensity.test.js test/reward-pipeline.test.js test/knn-cold-start.test.js test/calibration.test.js test/feedback-loop.test.js test/session-fingerprint.test.js test/side-request-guards.test.js test/verifier.test.js test/intent-score.test.js test/difficulty-classifier.test.js test/classifier-setup.test.js test/usage-stats.test.js test/loop-guard.test.js",
40
40
  "test:memory": "DATABRICKS_API_KEY=test-key DATABRICKS_API_BASE=http://test.com node --test test/memory/store.test.js test/memory/surprise.test.js test/memory/extractor.test.js test/memory/search.test.js test/memory/retriever.test.js",
41
41
  "test:new-features": "DATABRICKS_API_KEY=test-key DATABRICKS_API_BASE=http://test.com node --test test/passthrough-mode.test.js test/openrouter-error-resilience.test.js test/format-conversion.test.js",
42
42
  "test:performance": "DATABRICKS_API_KEY=test-key DATABRICKS_API_BASE=http://test.com node test/hybrid-routing-performance.test.js && DATABRICKS_API_KEY=test-key DATABRICKS_API_BASE=http://test.com node test/performance-tests.js",
@@ -124,4 +124,4 @@
124
124
  "nodemon": "^3.1.0",
125
125
  "pino-pretty": "^10.2.0"
126
126
  }
127
- }
127
+ }
@@ -0,0 +1,256 @@
1
+ #!/usr/bin/env node
2
+ /**
3
+ * Build the difficulty-classifier evaluation set.
4
+ *
5
+ * Sources (all zero-cost, no LLM judge required):
6
+ * 1. RouterArena unused rows — native empirical difficulty labels
7
+ * (easy/medium/hard from 42-model pass rate). No LLM involved.
8
+ * 2. routellm/gpt4_dataset unused rows — native mixtral_score 1–5
9
+ * (GPT-4-judged when the dataset was built). No LLM involved.
10
+ * 3. Hand-crafted coding-agent-flavored prompts — representative of
11
+ * Lynkr's real traffic (real logs proved too thin, ~7 unique prompts).
12
+ *
13
+ * Leak avoidance: skip any text already in data/difficulty-anchors.provenance.json
14
+ * (the mined anchor set) so eval and anchors don't overlap.
15
+ *
16
+ * Label → tier mapping:
17
+ * RouterArena easy → MEDIUM (any competent model handles)
18
+ * RouterArena medium → COMPLEX (needs strong general model)
19
+ * RouterArena hard → REASONING (frontier model territory)
20
+ * gpt4_dataset score 5 → MEDIUM (Mixtral got it right — trivially easy)
21
+ * gpt4_dataset score 3–4 → COMPLEX (Mixtral partial — real difficulty)
22
+ * gpt4_dataset score 1–2 → REASONING (Mixtral failed — top-tier work)
23
+ *
24
+ * Output: data/difficulty-eval.jsonl (gitignored)
25
+ * Usage: node scripts/build-eval-set.js
26
+ */
27
+
28
+ const fs = require("fs");
29
+ const path = require("path");
30
+
31
+ const DS_SERVER = "https://datasets-server.huggingface.co/rows";
32
+ const OUT_FILE = path.join(__dirname, "../data/difficulty-eval.jsonl");
33
+ const PROVENANCE = path.join(__dirname, "../data/difficulty-anchors.provenance.json");
34
+
35
+ const TARGETS = { routerarena: 200, gpt4_dataset: 200 };
36
+
37
+ function sleep(ms) { return new Promise(r => setTimeout(r, ms)); }
38
+
39
+ async function fetchRows(dataset, config, split, offset, length) {
40
+ const url = `${DS_SERVER}?dataset=${encodeURIComponent(dataset)}&config=${config}&split=${split}&offset=${offset}&length=${length}`;
41
+ for (let a = 0; a < 5; a++) {
42
+ try {
43
+ const res = await fetch(url);
44
+ if (res.status === 429) { await sleep(3000 * (a + 1)); continue; }
45
+ if (!res.ok) throw new Error(`HTTP ${res.status}`);
46
+ return (await res.json()).rows?.map(r => r.row) || [];
47
+ } catch (err) {
48
+ if (a === 4) return [];
49
+ await sleep(1500 * (a + 1));
50
+ }
51
+ }
52
+ return [];
53
+ }
54
+
55
+ function clean(text) {
56
+ if (typeof text !== "string") return null;
57
+ const t = text.replace(/\s+/g, " ").trim();
58
+ if (t.length < 15 || t.length > 400) return null;
59
+ if (/\(\s*\)/.test(t) || /_{3,}/.test(t)) return null;
60
+ if (/\b[A-D]\)\s/.test(t) || /which of the following/i.test(t)) return null;
61
+ if (/\b(premise|hypothesis)\b/i.test(t) && /\b(entail|inference|contradict)\b/i.test(t)) return null;
62
+ if (/NAME_\d|\{\{.*\}\}/.test(t)) return null;
63
+ const nonAscii = (t.match(/[^\x20-\x7E]/g) || []).length;
64
+ if (nonAscii > t.length * 0.1) return null;
65
+ return t;
66
+ }
67
+
68
+ function isQuizbowlStyle(t) {
69
+ return /^(this |one |a \d{4} (book|work|paper)|fictional |in one (work|section))/i.test(t) ||
70
+ /\([A-Za-z]+-[A-Z][A-Za-z-]+\)/.test(t);
71
+ }
72
+
73
+ // Hand-crafted coding-agent-flavored prompts by tier. Reflects real Lynkr
74
+ // traffic shape (imperatives, technical vocabulary, mix of simple/mechanical/
75
+ // systemic/rigorous). Labels are mine — the honest reviewer for this session.
76
+ const HAND_EVAL = [
77
+ // SIMPLE — casual/acks
78
+ { text: "hi", tier: "SIMPLE" },
79
+ { text: "hello", tier: "SIMPLE" },
80
+ { text: "ok thanks", tier: "SIMPLE" },
81
+ { text: "yes continue", tier: "SIMPLE" },
82
+ { text: "no skip that", tier: "SIMPLE" },
83
+ { text: "got it", tier: "SIMPLE" },
84
+ { text: "sure go ahead", tier: "SIMPLE" },
85
+ { text: "what time is it", tier: "SIMPLE" },
86
+ { text: "cool", tier: "SIMPLE" },
87
+ { text: "makes sense", tier: "SIMPLE" },
88
+ { text: "thanks a lot", tier: "SIMPLE" },
89
+ { text: "hey there", tier: "SIMPLE" },
90
+ { text: "yep proceed", tier: "SIMPLE" },
91
+ { text: "great work", tier: "SIMPLE" },
92
+ { text: "no worries", tier: "SIMPLE" },
93
+ // MEDIUM — one specific mechanical task
94
+ { text: "list the exports from src/router.js", tier: "MEDIUM" },
95
+ { text: "run the unit tests and summarize failures", tier: "MEDIUM" },
96
+ { text: "show me the git diff for this file", tier: "MEDIUM" },
97
+ { text: "what does this function do", tier: "MEDIUM" },
98
+ { text: "explain this regex pattern to me", tier: "MEDIUM" },
99
+ { text: "add error handling to this try block", tier: "MEDIUM" },
100
+ { text: "fix the linter warnings in this file", tier: "MEDIUM" },
101
+ { text: "write a unit test for the login helper", tier: "MEDIUM" },
102
+ { text: "convert this callback to async/await", tier: "MEDIUM" },
103
+ { text: "add a null check before this dereference", tier: "MEDIUM" },
104
+ { text: "rename this variable to something more descriptive", tier: "MEDIUM" },
105
+ { text: "extract this into a helper function", tier: "MEDIUM" },
106
+ { text: "add a docstring to this method", tier: "MEDIUM" },
107
+ { text: "check if this file has any TODO comments", tier: "MEDIUM" },
108
+ { text: "search the codebase for uses of deprecated API", tier: "MEDIUM" },
109
+ { text: "install the axios package as a dev dependency", tier: "MEDIUM" },
110
+ { text: "revert the last commit but keep the working tree", tier: "MEDIUM" },
111
+ { text: "delete unused imports from this file", tier: "MEDIUM" },
112
+ { text: "format this JSON blob nicely", tier: "MEDIUM" },
113
+ { text: "which files were changed in the last commit", tier: "MEDIUM" },
114
+ { text: "how do I use the fetch API with timeouts", tier: "MEDIUM" },
115
+ { text: "what's the difference between let and const in JavaScript", tier: "MEDIUM" },
116
+ { text: "add a rate limit to this endpoint", tier: "MEDIUM" },
117
+ { text: "grep for TODO in the routing folder", tier: "MEDIUM" },
118
+ { text: "make this loop use Promise.all instead of sequential awaits", tier: "MEDIUM" },
119
+ // COMPLEX — systemic / design / multi-file
120
+ { text: "do an architecture review of the orchestrator", tier: "COMPLEX" },
121
+ { text: "review this retry helper for bugs and race conditions", tier: "COMPLEX" },
122
+ { text: "refactor the entire ingestion pipeline and give me a plan", tier: "COMPLEX" },
123
+ { text: "design a horizontally scalable architecture for the router with failure-mode analysis", tier: "COMPLEX" },
124
+ { text: "code review the PR #84 routing hardening changes", tier: "COMPLEX" },
125
+ { text: "analyze every module in src/ for circular dependencies", tier: "COMPLEX" },
126
+ { text: "debug this complex race condition in the connection pool", tier: "COMPLEX" },
127
+ { text: "plan a zero-downtime migration to the new schema", tier: "COMPLEX" },
128
+ { text: "implement a distributed rate limiter with Redis backing", tier: "COMPLEX" },
129
+ { text: "design the caching strategy for this API gateway including invalidation rules", tier: "COMPLEX" },
130
+ { text: "trace through the request lifecycle end to end and identify bottlenecks", tier: "COMPLEX" },
131
+ { text: "restructure the module boundaries to reduce coupling between core and plugins", tier: "COMPLEX" },
132
+ { text: "propose a testing strategy for the new streaming pipeline covering edge cases", tier: "COMPLEX" },
133
+ { text: "identify all the places in the codebase that depend on the legacy auth flow and plan the deprecation", tier: "COMPLEX" },
134
+ { text: "walk me through how the tier routing decision cascade works and highlight the weak points", tier: "COMPLEX" },
135
+ { text: "compare our current queueing implementation against BullMQ and recommend a migration path", tier: "COMPLEX" },
136
+ { text: "extract the shared session logic into a separate package and update all consumers", tier: "COMPLEX" },
137
+ { text: "design the observability layer for the multi-tenant deployment with per-tenant metrics", tier: "COMPLEX" },
138
+ { text: "diagnose why the latency P99 spiked yesterday afternoon across the fleet", tier: "COMPLEX" },
139
+ { text: "plan the sharding strategy for the telemetry database as we scale to 10x traffic", tier: "COMPLEX" },
140
+ { text: "audit the codebase for uses of unsafe eval and propose safe replacements", tier: "COMPLEX" },
141
+ { text: "propose a graceful degradation plan for when the primary embedding model is unavailable", tier: "COMPLEX" },
142
+ { text: "review the error handling across the client SDK and standardize on a single pattern", tier: "COMPLEX" },
143
+ { text: "outline the migration from the monolith to a services split with backwards compatibility", tier: "COMPLEX" },
144
+ { text: "reason about whether this cache invalidation scheme is correct under concurrent writes", tier: "COMPLEX" },
145
+ // REASONING — proof / audit / formal / deep
146
+ { text: "prove the correctness of this lock-free queue implementation and identify any ABA hazards", tier: "REASONING" },
147
+ { text: "security audit the authentication middleware for token reuse vulnerabilities", tier: "REASONING" },
148
+ { text: "derive the optimal cache eviction policy for this access pattern and prove its competitive ratio", tier: "REASONING" },
149
+ { text: "formally verify that this state machine can never deadlock or produce a counterexample trace", tier: "REASONING" },
150
+ { text: "from first principles design a Byzantine-fault-tolerant consensus variant tolerating f faults with 2f+1 replicas", tier: "REASONING" },
151
+ { text: "think deeply about why this floating-point summation loses precision at scale and derive the compensated algorithm", tier: "REASONING" },
152
+ { text: "prove this rate limiter is fair under concurrent refill or construct the starvation schedule that breaks it", tier: "REASONING" },
153
+ { text: "reason through the exact memory ordering constraints this concurrent hashmap needs on ARM and justify each barrier", tier: "REASONING" },
154
+ { text: "given these heap dumps reason to the retention path causing the leak and rule out the two most plausible alternatives", tier: "REASONING" },
155
+ { text: "ultrathink: analyze whether this online schema migration can preserve every invariant the application relies on", tier: "REASONING" },
156
+ { text: "verify no path in this state machine violates the safety property that read never observes a partial write", tier: "REASONING" },
157
+ { text: "derive the amortized complexity bound for this splay tree under this adversarial access pattern and prove it tight", tier: "REASONING" },
158
+ { text: "prove that this distributed algorithm makes progress under fair scheduling regardless of message reordering", tier: "REASONING" },
159
+ { text: "penetration test the session token handling and enumerate every replay attack vector", tier: "REASONING" },
160
+ { text: "formal proof that this compensation transaction preserves ACID under partial-failure scenarios", tier: "REASONING" },
161
+ { text: "reason from first principles about whether exactly-once semantics are achievable on top of this at-least-once queue", tier: "REASONING" },
162
+ { text: "vulnerability scan the recently-added crypto path for downgrade attacks and misuse of primitives", tier: "REASONING" },
163
+ { text: "prove that this garbage collector will always terminate on any input given the described root set", tier: "REASONING" },
164
+ { text: "verify the linearizability of this concurrent skip list using the linearization-point argument", tier: "REASONING" },
165
+ { text: "think hard about which of these three algorithms actually preserves invariant I under crash recovery and which only appears to", tier: "REASONING" },
166
+ ];
167
+
168
+ async function main() {
169
+ const seenTexts = new Set();
170
+ try {
171
+ const prov = JSON.parse(fs.readFileSync(PROVENANCE, "utf8"));
172
+ for (const text of Object.keys(prov)) seenTexts.add(text);
173
+ console.log(`Loaded ${seenTexts.size} anchor texts from provenance (excluded from eval)`);
174
+ } catch { /* first run may have no provenance */ }
175
+
176
+ const eval_ = [];
177
+ const seen = new Set([...seenTexts].map(t => t.slice(0, 80)));
178
+
179
+ const add = (text, tier, source, label) => {
180
+ const key = text.slice(0, 80);
181
+ if (seen.has(key)) return false;
182
+ seen.add(key);
183
+ eval_.push({ text, tier, source, label });
184
+ return true;
185
+ };
186
+
187
+ // 1. Hand-crafted prompts (canary + real-traffic representative)
188
+ for (const item of HAND_EVAL) {
189
+ add(item.text, item.tier, "hand", "author-labeled");
190
+ }
191
+ console.log(`Hand-crafted: ${eval_.length} added`);
192
+
193
+ // 2. RouterArena — technical domains only, empirical difficulty labels
194
+ console.log("Fetching RouterArena rows...");
195
+ const TECH_DOMAIN_RE = /computer science|technology|engineering|mathematic|science/i;
196
+ const DATASET_EXCLUDE_RE = /superglue|cloze|entailment/i;
197
+ let raAdded = 0;
198
+ outer: for (let offset = 4000; offset < 8400; offset += 100) {
199
+ const rows = await fetchRows("RouteWorks/RouterArena", "default", "full", offset, 100);
200
+ for (const row of rows) {
201
+ if (raAdded >= TARGETS.routerarena) break outer;
202
+ if (row.Context && String(row.Context).trim()) continue;
203
+ if (Array.isArray(row.Options) && row.Options.length > 0) continue;
204
+ if (DATASET_EXCLUDE_RE.test(String(row["Dataset name"] || ""))) continue;
205
+ const text = clean(row.Question);
206
+ if (!text || isQuizbowlStyle(text)) continue;
207
+ const technical = TECH_DOMAIN_RE.test(String(row.Domain || ""));
208
+ const diff = String(row.Difficulty || "").toLowerCase();
209
+ let tier = null;
210
+ if (diff === "easy") tier = "MEDIUM";
211
+ else if (diff === "medium" && technical) tier = "COMPLEX";
212
+ else if (diff === "hard" && technical) tier = "REASONING";
213
+ if (!tier) continue;
214
+ if (add(text, tier, "RouterArena", `${diff}/${row["Dataset name"]}`)) raAdded++;
215
+ }
216
+ await sleep(200);
217
+ }
218
+ console.log(`RouterArena: +${raAdded}`);
219
+
220
+ // 3. gpt4_dataset — skip pages we mined for anchors (offset 0, 1817, 3634...)
221
+ console.log("Fetching gpt4_dataset rows...");
222
+ let gpAdded = 0;
223
+ outer2: for (let offset = 60000; offset < 109000; offset += 500) {
224
+ const rows = await fetchRows("routellm/gpt4_dataset", "default", "train", offset, 100);
225
+ for (const row of rows) {
226
+ if (gpAdded >= TARGETS.gpt4_dataset) break outer2;
227
+ const text = clean(row.prompt);
228
+ if (!text) continue;
229
+ const score = Number(row.mixtral_score);
230
+ let tier = null;
231
+ if (score === 5) tier = "MEDIUM";
232
+ else if (score === 3 || score === 4) tier = "COMPLEX";
233
+ else if (score === 1 || score === 2) tier = text.length < 60 ? null : "REASONING";
234
+ if (!tier) continue;
235
+ if (add(text, tier, "gpt4_dataset", `mixtral_score=${score}`)) gpAdded++;
236
+ }
237
+ await sleep(200);
238
+ }
239
+ console.log(`gpt4_dataset: +${gpAdded}`);
240
+
241
+ // Shuffle deterministically (session-repro via fixed permutation)
242
+ eval_.sort((a, b) => a.text.length - b.text.length);
243
+ const shuffled = [];
244
+ for (let i = 0; i < eval_.length; i++) {
245
+ const j = (i * 37 + 11) % eval_.length;
246
+ shuffled.push(eval_[j]);
247
+ }
248
+
249
+ fs.writeFileSync(OUT_FILE, shuffled.map(r => JSON.stringify(r)).join("\n") + "\n");
250
+ console.log(`\nWrote ${OUT_FILE}: ${shuffled.length} rows`);
251
+ const perTier = {};
252
+ for (const r of shuffled) perTier[r.tier] = (perTier[r.tier] || 0) + 1;
253
+ console.log("Per-tier:", JSON.stringify(perTier));
254
+ }
255
+
256
+ main().catch(err => { console.error(err); process.exit(1); });