lynkr 9.14.1 → 9.14.3

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (96) hide show
  1. package/README.md +5 -1
  2. package/bin/cli.js +14 -6
  3. package/bin/lynkr-desktop-token.js +108 -0
  4. package/bin/lynkr-init.js +13 -1
  5. package/bin/lynkr-restart.js +125 -0
  6. package/bin/opencode-setup.js +175 -0
  7. package/bin/run.js +92 -0
  8. package/config/model-capabilities.json +20 -0
  9. package/config/model-capability-seeds.json +75 -0
  10. package/config/model-tiers.json +13 -1
  11. package/package.json +16 -22
  12. package/scripts/claude-desktop.js +212 -0
  13. package/scripts/ope-report.js +78 -0
  14. package/scripts/seed-capabilities.js +230 -0
  15. package/src/api/claude-desktop-gateway.js +123 -0
  16. package/src/api/health.js +0 -1
  17. package/src/api/mcp-broker.js +149 -0
  18. package/src/api/middleware/budget-enforcer.js +77 -7
  19. package/src/api/middleware/budget.js +36 -2
  20. package/src/api/middleware/session.js +0 -0
  21. package/src/api/openai-router.js +210 -15
  22. package/src/api/providers-handler.js +16 -1
  23. package/src/api/router.js +73 -106
  24. package/src/auth-mode.js +40 -4
  25. package/src/budget/index.js +86 -0
  26. package/src/cache/embeddings.js +125 -35
  27. package/src/cache/onnx-embedder.js +92 -0
  28. package/src/cache/prompt.js +0 -7
  29. package/src/clients/bedrock-utils.js +2 -1
  30. package/src/clients/codex-process.js +1 -1
  31. package/src/clients/codex-utils.js +0 -2
  32. package/src/clients/databricks.js +337 -46
  33. package/src/clients/gpt-utils.js +84 -8
  34. package/src/clients/health-probe.js +157 -0
  35. package/src/clients/openai-format.js +73 -23
  36. package/src/clients/provider-capabilities.js +27 -2
  37. package/src/clients/resilience.js +0 -1
  38. package/src/clients/responses-format.js +0 -56
  39. package/src/clients/routing.js +1 -1
  40. package/src/config/index.js +13 -2
  41. package/src/context/budget.js +1 -1
  42. package/src/context/compression.js +127 -19
  43. package/src/context/tool-result-compressor.js +3 -5
  44. package/src/dashboard/api.js +1 -1
  45. package/src/logger/oversized-error-stream.js +1 -1
  46. package/src/memory/extractor.js +3 -3
  47. package/src/memory/search.js +2 -2
  48. package/src/memory/store.js +9 -24
  49. package/src/memory/tools.js +14 -0
  50. package/src/observability/metrics.js +52 -3
  51. package/src/observability/otel.js +180 -0
  52. package/src/orchestrator/azure-responses-sse.js +223 -0
  53. package/src/orchestrator/index.js +216 -212
  54. package/src/orchestrator/passthrough-stream.js +7 -1
  55. package/src/orchestrator/sse-transformer.js +130 -13
  56. package/src/policy/safe-commands.js +0 -1
  57. package/src/routing/bandit.js +39 -0
  58. package/src/routing/calibration.js +21 -1
  59. package/src/routing/capabilities.js +88 -0
  60. package/src/routing/capability-seeds/benchmarks-dir.js +58 -0
  61. package/src/routing/capability-seeds/family-heuristics.js +77 -0
  62. package/src/routing/capability-seeds/family.js +105 -0
  63. package/src/routing/capability-seeds/models-dev.js +64 -0
  64. package/src/routing/capability-seeds/normalize.js +160 -0
  65. package/src/routing/capability-seeds/registry.js +125 -0
  66. package/src/routing/capability-seeds/sources.js +69 -0
  67. package/src/routing/capability-seeds/swebench.js +66 -0
  68. package/src/routing/client-profiles.js +26 -0
  69. package/src/routing/complexity-analyzer.js +5 -8
  70. package/src/routing/confidence-scorer.js +2 -2
  71. package/src/routing/cost-optimizer.js +1 -2
  72. package/src/routing/decide.js +149 -0
  73. package/src/routing/deescalator.js +38 -1
  74. package/src/routing/embedding-cache.js +0 -1
  75. package/src/routing/index.js +202 -86
  76. package/src/routing/intent-score.js +0 -1
  77. package/src/routing/knn-router.js +27 -5
  78. package/src/routing/latency-tracker.js +1 -1
  79. package/src/routing/model-registry.js +25 -0
  80. package/src/routing/model-slots.js +65 -0
  81. package/src/routing/model-tiers.js +2 -0
  82. package/src/routing/ope.js +255 -0
  83. package/src/routing/openai-model-slots.js +82 -0
  84. package/src/routing/regret-estimator.js +1 -1
  85. package/src/routing/shortfall.js +290 -0
  86. package/src/routing/stuck-detector.js +127 -0
  87. package/src/routing/telemetry.js +11 -2
  88. package/src/server.js +41 -3
  89. package/src/stores/file-store.js +5 -1
  90. package/src/tools/web-search-exec.js +182 -0
  91. package/src/training/trajectory-compressor.js +1 -1
  92. package/src/workers/pool.js +1 -1
  93. package/src/workers/worker.js +1 -1
  94. package/scripts/convert-routellm.py +0 -105
  95. package/src/diff/comments.js +0 -153
  96. package/src/policy/web-fallback.js +0 -33
@@ -0,0 +1,64 @@
1
+ /**
2
+ * models.dev adapter: boolean capability flags per model (tool_call,
3
+ * vision/image input, reasoning, context window). Same public endpoint the
4
+ * pricing registry already consumes (https://models.dev/api.json), same
5
+ * shape ({ providerId: { models: { modelId: {...} } } }).
6
+ *
7
+ * Flags don't set caps — they GATE them: a model with tool_call=false gets
8
+ * tool_use capped (it can still drive tools via XML extraction, which is why
9
+ * this is a cap, not a zero). Everything else passes through as provenance
10
+ * for operator review.
11
+ *
12
+ * Fail-soft: { status:'skipped', reason } on any problem.
13
+ */
14
+
15
+ const { fetchJson, getCached, setCached, skipped } = require('./sources');
16
+ const { normalizeFamily } = require('./family');
17
+
18
+ const MODELS_DEV_URL = 'https://models.dev/api.json';
19
+
20
+ // Conservative ceiling for models without function-calling. XML-extracted
21
+ // tool calls (see xml-tool-extractor.js) keep them usable, hence 0.4 not 0.
22
+ const NO_TOOLCALL_TOOL_USE_CAP = 0.4;
23
+
24
+ /**
25
+ * @param {object} [opts] — { refresh:boolean }
26
+ * @returns {Promise<{ status, flags?:Object<family,{toolCall,vision,reasoning,context}>, reason? }>}
27
+ */
28
+ async function fetchModelsDevFlags({ refresh = true } = {}) {
29
+ const parse = (data) => {
30
+ const out = {};
31
+ for (const [providerId, providerData] of Object.entries(data || {})) {
32
+ if (!providerData?.models) continue;
33
+ for (const [modelId, info] of Object.entries(providerData.models)) {
34
+ if (!info || typeof info !== 'object') continue;
35
+ const { family } = normalizeFamily(providerId, modelId);
36
+ if (family === 'unknown' || out[family]) continue; // first provider wins
37
+ out[family] = {
38
+ toolCall: info.tool_call ?? null,
39
+ vision: Array.isArray(info.input) ? info.input.includes('image') : null,
40
+ reasoning: info.reasoning ?? null,
41
+ context: Number(info.context) || null,
42
+ };
43
+ }
44
+ }
45
+ return out;
46
+ };
47
+
48
+ if (!refresh) {
49
+ const cached = getCached('modelsdev');
50
+ if (cached) return { status: 'ok', flags: cached, cached: true };
51
+ return skipped('no cache (run with --refresh)');
52
+ }
53
+ try {
54
+ const flags = parse(await fetchJson(MODELS_DEV_URL));
55
+ setCached('modelsdev', flags);
56
+ return { status: 'ok', flags };
57
+ } catch (err) {
58
+ const cached = getCached('modelsdev');
59
+ if (cached) return { status: 'ok', flags: cached, cached: true, stale: err.message };
60
+ return skipped(`fetch failed and no cache: ${err.message}`);
61
+ }
62
+ }
63
+
64
+ module.exports = { fetchModelsDevFlags, MODELS_DEV_URL, NO_TOOLCALL_TOOL_USE_CAP };
@@ -0,0 +1,160 @@
1
+ /**
2
+ * Score → capability-cap normalization + leaderboard-name → family mapping.
3
+ *
4
+ * Anchors (fixed permanently — τ absorbs residual error, consistency matters
5
+ * more than precision): benchmark fraction 0.8 (≈ flagship SWE-Verified
6
+ * territory) → caps 0.9; fraction 0 → 0.15 (never zero: even weak models do
7
+ * trivial turns). Linear between, clamped to [0.15, 0.9].
8
+ *
9
+ * Per-head source weights (v1 heuristic, documented):
10
+ * debugging = 0.6·swe + 0.4·terminal
11
+ * codegen = 0.5·livecode + 0.3·swe + 0.2·arena
12
+ * reasoning = 0.5·swe + 0.3·arena + 0.2·livecode
13
+ * tool_use = 0.6·terminal + 0.4·swe
14
+ * Missing sources renormalize over the present ones. A pessimistic haircut
15
+ * (-0.03) applies to all normalized caps: underestimating a new model costs
16
+ * money (extra escalations), overestimating costs quality.
17
+ *
18
+ * Family mapping: order-insensitive token overlap between the normalized
19
+ * family id and the leaderboard name. Every family token must hit an entry
20
+ * token exactly or as a version prefix ("4" matches "4.5", "k2" matches
21
+ * "k2.5" — but "32b" never matches "480b"). Best overlap wins; weak matches
22
+ * land in `unmapped` for operator review instead of being seeded silently.
23
+ * Pure functions, never throw.
24
+ */
25
+
26
+ const { normalizeFamily } = require('./family');
27
+
28
+ const CAP_FLOOR = 0.15;
29
+ const CAP_CEIL = 0.9;
30
+ const NORMALIZE_HAIRCUT = 0.03;
31
+
32
+ // score fraction (0-1) at which caps hit the ceiling — flagship territory.
33
+ const ANCHOR_SCORE = 0.8;
34
+
35
+ const HEAD_WEIGHTS = {
36
+ debugging: { swe: 0.6, terminal: 0.4, livecode: 0, arena: 0 },
37
+ codegen: { swe: 0.3, terminal: 0, livecode: 0.5, arena: 0.2 },
38
+ reasoning: { swe: 0.5, terminal: 0, livecode: 0.2, arena: 0.3 },
39
+ tool_use: { swe: 0.4, terminal: 0.6, livecode: 0, arena: 0 },
40
+ };
41
+
42
+ const STOPWORDS = new Set([
43
+ 'the', 'model', 'preview', 'high', 'medium', 'low',
44
+ 'agent', 'instruct', 'thinking',
45
+ '2024', '2025', '2026',
46
+ ]);
47
+
48
+ function entryTokens(name) {
49
+ return String(name || '')
50
+ .toLowerCase()
51
+ .split(/[^a-z0-9.]+/)
52
+ .map((t) => t.replace(/^\.+|\.+$/g, ''))
53
+ .filter((t) => t && !STOPWORDS.has(t));
54
+ }
55
+
56
+ function _tokenHit(familyTok, entryToks) {
57
+ for (const tok of entryToks) {
58
+ if (tok === familyTok) return true;
59
+ // version prefix: family "4" hits entry "4.5"; family "k2" hits "k2.5".
60
+ // The char after the prefix must be a dot (never a letter/digit, so
61
+ // "32b" can't match "480b" and "gpt-5" can't match "gpt-50").
62
+ if (tok.length > familyTok.length && tok.startsWith(familyTok) && tok[familyTok.length] === '.') {
63
+ return true;
64
+ }
65
+ // reverse: entry "4" satisfies family "4.5"? No — a bare major never
66
+ // proves the minor. Skip.
67
+ }
68
+ return false;
69
+ }
70
+
71
+ function familyTokens(family) {
72
+ return String(family || '')
73
+ .toLowerCase()
74
+ .split(/[^a-z0-9.]+/)
75
+ .map((t) => t.replace(/^\.+|\.+$/g, ''))
76
+ .filter((t) => t && !STOPWORDS.has(t));
77
+ }
78
+
79
+ /**
80
+ * @param {string} entryName — cleaned leaderboard name
81
+ * @param {string[]} knownFamilies — candidate family ids (seed keys w/o wildcards)
82
+ * @returns {{ family:string|null, hits:number, of:number }}
83
+ */
84
+ function mapEntryToFamily(entryName, knownFamilies) {
85
+ try {
86
+ const toks = entryTokens(entryName);
87
+ if (toks.length === 0 || !Array.isArray(knownFamilies)) {
88
+ return { family: null, hits: 0, of: 0 };
89
+ }
90
+ let best = { family: null, hits: 0, of: 0, ratio: 0 };
91
+ for (const fam of knownFamilies) {
92
+ const ftoks = familyTokens(fam);
93
+ if (ftoks.length === 0) continue;
94
+ let hits = 0;
95
+ for (const ft of ftoks) {
96
+ if (_tokenHit(ft, toks)) hits++;
97
+ }
98
+ const ratio = hits / ftoks.length;
99
+ // Full coverage required; ties prefer the more specific family.
100
+ if (ratio === 1 && (best.family === null || ftoks.length > best.of)) {
101
+ best = { family: fam, hits, of: ftoks.length, ratio };
102
+ }
103
+ }
104
+ return best.family ? best : { family: null, hits: 0, of: 0 };
105
+ } catch {
106
+ return { family: null, hits: 0, of: 0 };
107
+ }
108
+ }
109
+
110
+ function scoreToCap(score) {
111
+ const s = Math.max(0, Math.min(1, Number(score)));
112
+ if (!Number.isFinite(s)) return null;
113
+ const cap = CAP_FLOOR + (s / ANCHOR_SCORE) * (CAP_CEIL - CAP_FLOOR);
114
+ return Math.max(CAP_FLOOR, Math.min(CAP_CEIL, cap));
115
+ }
116
+
117
+ /**
118
+ * @param {object} scores — { swe?, terminal?, livecode?, arena? } fractions 0-1
119
+ * @returns {null | { reasoning, codegen, debugging, tool_use }}
120
+ */
121
+ function scoresToCaps(scores) {
122
+ try {
123
+ if (!scores || typeof scores !== 'object') return null;
124
+ const present = {};
125
+ for (const k of ['swe', 'terminal', 'livecode', 'arena']) {
126
+ const v = Number(scores[k]);
127
+ if (Number.isFinite(v)) present[k] = Math.max(0, Math.min(1, v));
128
+ }
129
+ if (Object.keys(present).length === 0) return null;
130
+ const caps = {};
131
+ for (const [head, weights] of Object.entries(HEAD_WEIGHTS)) {
132
+ let total = 0;
133
+ let wsum = 0;
134
+ for (const [src, w] of Object.entries(weights)) {
135
+ if (present[src] !== undefined && w > 0) {
136
+ total += scoreToCap(present[src]) * w;
137
+ wsum += w;
138
+ }
139
+ }
140
+ if (wsum === 0) return null;
141
+ caps[head] = Math.round((total / wsum - NORMALIZE_HAIRCUT) * 1000) / 1000;
142
+ caps[head] = Math.max(CAP_FLOOR, Math.min(CAP_CEIL, caps[head]));
143
+ }
144
+ return caps;
145
+ } catch {
146
+ return null;
147
+ }
148
+ }
149
+
150
+ module.exports = {
151
+ mapEntryToFamily,
152
+ scoresToCaps,
153
+ scoreToCap,
154
+ entryTokens,
155
+ familyTokens,
156
+ normalizeFamily,
157
+ CAP_FLOOR,
158
+ CAP_CEIL,
159
+ NORMALIZE_HAIRCUT,
160
+ };
@@ -0,0 +1,125 @@
1
+ /**
2
+ * Seed registry: per-FAMILY capability profiles from online-derived data.
3
+ *
4
+ * Layers (first hit wins in shortfall.js resolveCapabilities):
5
+ * operator modelOverrides (provider:model, manual — supreme, lives in
6
+ * model-capabilities.json) → snapshot (data/, fetched, `source:'seed:snapshot'`)
7
+ * → shipped (config/, reviewed, `source:'seed:shipped'`) → family
8
+ * heuristic (`source:'family'`) → tier slot (`source:'tier'`).
9
+ *
10
+ * Keys are normalized family ids (see family.js), lowercased, with optional
11
+ * trailing `*` wildcards matched longest-first (gpt-5* , qwen3-*). Provider
12
+ * never appears in keys — zai/baidu/ollama servings of glm-5.2 share one entry.
13
+ *
14
+ * File lifecycles mirror model-tiers.json: read once at boot, restart to
15
+ * pick up edits. data/ is gitignored (operator-local), config/ is versioned.
16
+ * Pure lookups after load; load never throws (missing/malformed → empty).
17
+ */
18
+
19
+ const fs = require('fs');
20
+ const path = require('path');
21
+ const logger = require('../../logger');
22
+ const { HEADS } = require('../capabilities');
23
+
24
+ const SHIPPED_PATH = path.join(__dirname, '../../../config/model-capability-seeds.json');
25
+ const SNAPSHOT_PATH = path.join(__dirname, '../../../data/capability-seeds.snapshot.json');
26
+
27
+ let _cache = null;
28
+
29
+ function sanitizeCaps(raw) {
30
+ const caps = {};
31
+ for (const h of HEADS) {
32
+ const v = Number(raw?.[h]);
33
+ caps[h] = Number.isFinite(v) ? Math.max(0, Math.min(1, v)) : 0.5;
34
+ }
35
+ return caps;
36
+ }
37
+
38
+ function _loadSeedsFile(filePath, label) {
39
+ try {
40
+ if (!fs.existsSync(filePath)) return {};
41
+ const raw = JSON.parse(fs.readFileSync(filePath, 'utf8'));
42
+ const seeds = raw?.seeds && typeof raw.seeds === 'object' ? raw.seeds : {};
43
+ const out = {};
44
+ for (const [key, entry] of Object.entries(seeds)) {
45
+ const caps = entry?.caps && typeof entry.caps === 'object' ? entry.caps : entry;
46
+ if (!caps || typeof caps !== 'object') continue;
47
+ out[String(key).toLowerCase()] = {
48
+ caps: sanitizeCaps(caps),
49
+ sources: Array.isArray(entry?.sources) ? entry.sources : [],
50
+ note: typeof entry?.note === 'string' ? entry.note : '',
51
+ };
52
+ }
53
+ return out;
54
+ } catch (err) {
55
+ logger.debug({ err: err.message, label }, '[SeedRegistry] seed file load failed — skipping layer');
56
+ return {};
57
+ }
58
+ }
59
+
60
+ function loadSeedLayers() {
61
+ if (_cache) return _cache;
62
+ _cache = {
63
+ snapshot: _loadSeedsFile(SNAPSHOT_PATH, 'snapshot'),
64
+ shipped: _loadSeedsFile(SHIPPED_PATH, 'shipped'),
65
+ };
66
+ return _cache;
67
+ }
68
+
69
+ function _resetSeedsCache() {
70
+ _cache = null;
71
+ }
72
+
73
+ // Test hook: inject layers directly (Operations on the same cached object
74
+ // the resolver reads).
75
+ function _setSeedsForTests({ snapshot, shipped } = {}) {
76
+ const base = loadSeedLayers();
77
+ _cache = {
78
+ snapshot: snapshot ?? base.snapshot,
79
+ shipped: shipped ?? base.shipped,
80
+ };
81
+ return _cache;
82
+ }
83
+
84
+ function _matchWildcard(layer, family) {
85
+ let best = null;
86
+ for (const [key, entry] of Object.entries(layer)) {
87
+ if (!key.endsWith('*')) continue;
88
+ const prefix = key.slice(0, -1);
89
+ if (prefix && family.startsWith(prefix) && (!best || prefix.length > best.prefix.length)) {
90
+ best = { prefix, entry };
91
+ }
92
+ }
93
+ return best?.entry ?? null;
94
+ }
95
+
96
+ /**
97
+ * @param {string} family — normalized family id
98
+ * @returns {null | { caps, source:'seed:snapshot'|'seed:shipped', sources, note }}
99
+ */
100
+ function resolveSeedCaps(family) {
101
+ try {
102
+ const f = String(family || '').toLowerCase().trim();
103
+ if (!f || f === 'unknown') return null;
104
+ const { snapshot, shipped } = loadSeedLayers();
105
+ if (snapshot[f]) return { ...snapshot[f], source: 'seed:snapshot' };
106
+ if (shipped[f]) return { ...shipped[f], source: 'seed:shipped' };
107
+ const snapWild = _matchWildcard(snapshot, f);
108
+ if (snapWild) return { ...snapWild, source: 'seed:snapshot' };
109
+ const shipWild = _matchWildcard(shipped, f);
110
+ if (shipWild) return { ...shipWild, source: 'seed:shipped' };
111
+ return null;
112
+ } catch {
113
+ return null;
114
+ }
115
+ }
116
+
117
+ module.exports = {
118
+ loadSeedLayers,
119
+ _resetSeedsCache,
120
+ _setSeedsForTests,
121
+ resolveSeedCaps,
122
+ sanitizeCaps,
123
+ SHIPPED_PATH,
124
+ SNAPSHOT_PATH,
125
+ };
@@ -0,0 +1,69 @@
1
+ /**
2
+ * Shared plumbing for capability-seed sources (online benchmark data).
3
+ *
4
+ * Every adapter is optional and fail-soft: no network, bad payload, or
5
+ * unknown shape → { status:'skipped', reason } — never throws, never blocks
6
+ * the script, and never touches request-path routing (this only runs in
7
+ * scripts/seed-capabilities.js). Fetched payloads are cached under
8
+ * data/capability-sources-cache.json with a 7-day TTL so refreshes are
9
+ * cheap and CI stays hermetic (tests use fixtures, never the network).
10
+ */
11
+
12
+ const fs = require('fs');
13
+ const path = require('path');
14
+
15
+ const CACHE_PATH = path.join(__dirname, '../../../data/capability-sources-cache.json');
16
+ const CACHE_TTL_MS = 7 * 24 * 60 * 60 * 1000;
17
+ const FETCH_TIMEOUT_MS = 20000;
18
+
19
+ function _readCache() {
20
+ try {
21
+ if (!fs.existsSync(CACHE_PATH)) return {};
22
+ return JSON.parse(fs.readFileSync(CACHE_PATH, 'utf8'));
23
+ } catch {
24
+ return {};
25
+ }
26
+ }
27
+
28
+ function _writeCache(cache) {
29
+ try {
30
+ fs.mkdirSync(path.dirname(CACHE_PATH), { recursive: true });
31
+ fs.writeFileSync(CACHE_PATH, JSON.stringify(cache, null, 2));
32
+ } catch {
33
+ // cache is best-effort; a failed write must not fail seeding
34
+ }
35
+ }
36
+
37
+ function getCached(source) {
38
+ const cache = _readCache();
39
+ const entry = cache[source];
40
+ if (!entry || typeof entry !== 'object') return null;
41
+ if (Date.now() - (entry.fetchedAt || 0) > CACHE_TTL_MS) return null;
42
+ return entry.payload ?? null;
43
+ }
44
+
45
+ function setCached(source, payload) {
46
+ const cache = _readCache();
47
+ cache[source] = { fetchedAt: Date.now(), payload };
48
+ _writeCache(cache);
49
+ }
50
+
51
+ async function fetchJson(url, { timeoutMs = FETCH_TIMEOUT_MS } = {}) {
52
+ const response = await fetch(url, {
53
+ signal: AbortSignal.timeout(timeoutMs),
54
+ headers: { Accept: 'application/json' },
55
+ });
56
+ if (!response.ok) throw new Error(`HTTP ${response.status}`);
57
+ return response.json();
58
+ }
59
+
60
+ const skipped = (reason) => ({ status: 'skipped', reason });
61
+
62
+ module.exports = {
63
+ CACHE_PATH,
64
+ CACHE_TTL_MS,
65
+ getCached,
66
+ setCached,
67
+ fetchJson,
68
+ skipped,
69
+ };
@@ -0,0 +1,66 @@
1
+ /**
2
+ * SWE-Bench Verified adapter: per-model % resolved from the public
3
+ * leaderboard raw data.
4
+ *
5
+ * Source: https://raw.githubusercontent.com/SWE-bench/swe-bench.github.io/master/data/leaderboards.json
6
+ * Shape: { leaderboards: [{ name: "Verified"|"Lite"|..., results: [{ name, resolved, date, ... }] }] }
7
+ * Entry names look like "Claude 4.5 Opus (high) · mini-SWE-agent · 2026-02-17"
8
+ * (model + effort qualifier + scaffold + date). We keep the best resolved%
9
+ * per cleaned model name; family mapping happens in normalize.js
10
+ * (token-overlap against known families, unmapped surfaced for review).
11
+ *
12
+ * Fail-soft: any fetch/parse problem → { status:'skipped', reason }.
13
+ */
14
+
15
+ const { fetchJson, getCached, setCached, skipped } = require('./sources');
16
+
17
+ const LEADERBOARDS_URL = 'https://raw.githubusercontent.com/SWE-bench/swe-bench.github.io/master/data/leaderboards.json';
18
+ const BOARD = 'Verified';
19
+
20
+ function cleanName(name) {
21
+ return String(name || '')
22
+ .split('·')[0] // drop scaffold + date segments
23
+ .replace(/\((high|medium|low)\)/gi, '') // effort qualifier (we keep max across them)
24
+ .trim()
25
+ .replace(/\s+/g, ' ')
26
+ .toLowerCase();
27
+ }
28
+
29
+ /**
30
+ * @param {object} [opts] — { refresh:boolean } (false = cache-only)
31
+ * @returns {Promise<{ status, board?, entries?:Array<{name,resolved,date}>, reason? }>}
32
+ */
33
+ async function fetchSweBench({ refresh = true } = {}) {
34
+ if (!refresh) {
35
+ const cached = getCached('swebench');
36
+ if (cached) return { status: 'ok', board: BOARD, entries: cached, cached: true };
37
+ return skipped('no cache (run with --refresh)');
38
+ }
39
+ try {
40
+ const data = await fetchJson(LEADERBOARDS_URL);
41
+ const boards = Array.isArray(data?.leaderboards) ? data.leaderboards : [];
42
+ const verified = boards.find((b) => String(b?.name || '').toLowerCase() === BOARD.toLowerCase());
43
+ if (!verified || !Array.isArray(verified.results)) {
44
+ return skipped(`board "${BOARD}" not found in payload`);
45
+ }
46
+ const best = new Map();
47
+ for (const r of verified.results) {
48
+ const name = cleanName(r?.name);
49
+ const resolved = Number(r?.resolved);
50
+ if (!name || !Number.isFinite(resolved)) continue;
51
+ const prev = best.get(name);
52
+ if (!prev || resolved > prev.resolved) {
53
+ best.set(name, { name, resolved, date: r?.date ?? null });
54
+ }
55
+ }
56
+ const entries = [...best.values()];
57
+ setCached('swebench', entries);
58
+ return { status: 'ok', board: BOARD, entries };
59
+ } catch (err) {
60
+ const cached = getCached('swebench');
61
+ if (cached) return { status: 'ok', board: BOARD, entries: cached, cached: true, stale: err.message };
62
+ return skipped(`fetch failed and no cache: ${err.message}`);
63
+ }
64
+ }
65
+
66
+ module.exports = { fetchSweBench, cleanName, LEADERBOARDS_URL, BOARD };
@@ -114,6 +114,32 @@ const PROFILES = {
114
114
  minToolFingerprintMatch: 0.5,
115
115
  },
116
116
  },
117
+ 'opencode': {
118
+ name: 'opencode',
119
+ baselineTools: new Set([
120
+ // OpenCode 1.18.x default loadout — captured live 2026-08-22 from
121
+ // Lynkr logs (user-agent `opencode/1.18.21 ai-sdk/provider-utils/…`).
122
+ // All of these attach to every request; counting them as intent
123
+ // scored every OpenCode turn ~80 → REASONING band → session pin
124
+ // locked the whole session there (2026-08-22: 1163/1203 REASONING
125
+ // routings were pin-inherited from a floored first turn).
126
+ 'bash', 'edit', 'glob', 'grep', 'question', 'read', 'skill', 'task',
127
+ 'todowrite', 'webfetch', 'write',
128
+ // Per opencode.ai/docs/tools — enabled-by-default or common-config
129
+ // names not seen in our capture; harmless if absent (subtraction
130
+ // only removes names actually present).
131
+ 'list', 'todoread', 'websearch',
132
+ ]),
133
+ detect: {
134
+ headerPatterns: [/opencode/i],
135
+ // 0.5 (like goose): `question`/`skill`/`todo*` vary with user config,
136
+ // so a minimal loadout matches ~8 of 14 baseline names. The lowercase
137
+ // set is disjoint from claude-code's capitalized and cursor's
138
+ // snake_case names, and opencode always sends its user-agent, so the
139
+ // header pattern is the primary detector anyway.
140
+ minToolFingerprintMatch: 0.5,
141
+ },
142
+ },
117
143
  'openai-codex': {
118
144
  name: 'openai-codex',
119
145
  baselineTools: new Set([
@@ -44,7 +44,7 @@ function _effectiveTools(payload) {
44
44
  // Pre-compiled regex patterns for performance
45
45
  const PATTERNS = {
46
46
  // Greetings - always local
47
- greeting: /^(hi|hello|hey|good\s*(morning|afternoon|evening)|howdy|greetings|sup|yo|thanks?|thank\s*you)[\s\.\!\?,]*$/i,
47
+ greeting: /^(hi|hello|hey|good\s*(morning|afternoon|evening)|howdy|greetings|sup|yo|thanks?|thank\s*you)[\s.!?,]*$/i,
48
48
 
49
49
  // Simple questions - likely local
50
50
  simpleQuestion: /^(what\s+is|what's|define|who\s+is|when\s+was|where\s+is)\s+\w/i,
@@ -56,7 +56,7 @@ const PATTERNS = {
56
56
  technical: /\b(code|function|class|method|variable|api|database|server|client|module|import|export|async|await|promise|component|interface|type|struct|enum)\b/i,
57
57
 
58
58
  // File/path references
59
- fileReference: /\b(\w+\.(js|ts|py|rb|go|rs|java|cpp|c|h|jsx|tsx|vue|svelte|md|json|yaml|yml|toml|sql|sh|bash))\b|[\.\/]\w+\//i,
59
+ fileReference: /\b(\w+\.(js|ts|py|rb|go|rs|java|cpp|c|h|jsx|tsx|vue|svelte|md|json|yaml|yml|toml|sql|sh|bash))\b|[./]\w+\//i,
60
60
  };
61
61
 
62
62
  // ============================================================================
@@ -119,10 +119,10 @@ const FORCE_CLOUD_PATTERNS = [
119
119
 
120
120
  // Force local patterns - always route to local regardless of score
121
121
  const FORCE_LOCAL_PATTERNS = [
122
- /^(hi|hello|hey|thanks?|thank\s*you|bye|goodbye)[\s\.\!\?]*$/i,
122
+ /^(hi|hello|hey|thanks?|thank\s*you|bye|goodbye)[\s.!?]*$/i,
123
123
  /^what\s+(time|day|date)\s+is\s+it/i,
124
- /^(yes|no|ok|okay|sure|got\s+it|understood)[\s\.\!\?]*$/i,
125
- /^(help|menu|commands?|options?)[\s\.\!\?]*$/i,
124
+ /^(yes|no|ok|okay|sure|got\s+it|understood)[\s.!?]*$/i,
125
+ /^(help|menu|commands?|options?)[\s.!?]*$/i,
126
126
  ];
127
127
 
128
128
  // Weighted Scoring (15 Dimensions)
@@ -478,8 +478,6 @@ function scoreTools(payload) {
478
478
  * Score based on task type (0-25 points)
479
479
  */
480
480
  function scoreTaskType(content) {
481
- const contentLower = content.toLowerCase();
482
-
483
481
  // Check force patterns first (priority: REASONING > cloud > local)
484
482
  for (const pattern of FORCE_REASONING_PATTERNS) {
485
483
  if (pattern.test(content)) {
@@ -796,7 +794,6 @@ async function analyzeComplexity(payload, options = {}) {
796
794
  if (useWeighted) {
797
795
  const weighted = calculateWeightedScore(workingPayload, content);
798
796
  const threshold = getThreshold();
799
- const mode = config.smartToolSelection?.mode ?? 'heuristic';
800
797
 
801
798
  // Check force patterns
802
799
  const taskTypeResult = scoreTaskType(content);
@@ -82,8 +82,8 @@ function scoreCode(response) {
82
82
  const fenced = (text.match(/```[\s\S]*?```/g) || []).join('\n');
83
83
  if (!fenced) return 0.6; // Code-gen request without code is suspicious
84
84
  // Very basic balance check
85
- const opens = (fenced.match(/[\{\[\(]/g) || []).length;
86
- const closes = (fenced.match(/[\}\]\)]/g) || []).length;
85
+ const opens = (fenced.match(/[{[(]/g) || []).length;
86
+ const closes = (fenced.match(/[}\])]/g) || []).length;
87
87
  if (Math.abs(opens - closes) > 2) return 0.5;
88
88
  return 0.9;
89
89
  }
@@ -5,9 +5,8 @@
5
5
  */
6
6
 
7
7
  const logger = require('../logger');
8
- const config = require('../config');
9
8
  const { getModelRegistry, getModelRegistrySync } = require('./model-registry');
10
- const { getModelTierSelector, TIER_DEFINITIONS } = require('./model-tiers');
9
+ const { getModelTierSelector } = require('./model-tiers');
11
10
  const { ratioFor } = require('./output-ratios');
12
11
 
13
12
  // Session cost tracking (in-memory)