@kal-elsam/kairo-runtime 0.15.0 → 0.17.0
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/CHANGELOG.md +76 -0
- package/package.json +2 -1
- package/scripts/cockpit-smoke.mjs +1 -1
- package/scripts/ux-smoke-test.sh +3 -3
- package/src/cli.js +106 -11
- package/src/global/agent-capabilities/create-capability-adapter.js +2 -2
- package/src/global/architect/architect-cli.js +76 -0
- package/src/global/architect/architect-codex.js +146 -0
- package/src/global/architect/architect-manager.js +125 -0
- package/src/global/architect/architect-store.js +377 -0
- package/src/global/architect/architect-types.js +47 -0
- package/src/global/cli-help.js +12 -1
- package/src/global/cockpit/app.js +475 -0
- package/src/global/cockpit/card.js +111 -0
- package/src/global/cockpit/cli.js +33 -0
- package/src/global/cockpit/gauge.js +31 -0
- package/src/global/cockpit/project-overlay.js +683 -0
- package/src/global/cockpit/rows.js +148 -0
- package/src/global/cockpit/theme.js +118 -0
- package/src/global/cockpit/view.js +1263 -0
- package/src/global/control-plane/attention.js +141 -0
- package/src/global/control-plane/build-report.js +146 -0
- package/src/global/control-plane/cli.js +36 -0
- package/src/global/control-plane/constants.js +38 -0
- package/src/global/control-plane/gentle-adapters.js +183 -0
- package/src/global/control-plane/provider.js +69 -0
- package/src/global/control-plane/review-status.js +115 -0
- package/src/global/control-plane/sdd-status.js +49 -0
- package/src/global/control-plane/team.js +63 -0
- package/src/global/conversation/bootstrap-analyzer-adapters.js +251 -0
- package/src/global/conversation/cli.js +53 -0
- package/src/global/conversation/codex-sandbox.js +230 -0
- package/src/global/conversation/cursor-sandbox.js +215 -0
- package/src/global/conversation/project-analysis.js +204 -0
- package/src/global/conversation/project-profile.js +178 -0
- package/src/global/conversation/project-router.js +149 -0
- package/src/global/conversation/project-strategy-store.js +64 -0
- package/src/global/conversation/project-strategy.js +514 -0
- package/src/global/conversation/sanitized-snapshot.js +169 -0
- package/src/global/conversation/secret-scanner.js +71 -0
- package/src/global/conversation/service.js +1063 -0
- package/src/global/conversation/session-store.js +75 -0
- package/src/global/conversation/transcript-store.js +79 -0
- package/src/global/conversation/ui.js +195 -0
- package/src/global/intelligence/capability-scoring.js +480 -0
- package/src/global/intelligence/execution-router.js +444 -0
- package/src/global/intelligence/kairo-telemetry-source.js +59 -0
- package/src/global/intelligence/kairobench-runner.js +85 -0
- package/src/global/intelligence/kairobench-source.js +34 -0
- package/src/global/intelligence/kairobench-tasks.js +47 -0
- package/src/global/intelligence/model-candidate-catalog.js +456 -0
- package/src/global/intelligence/model-capability-registry-sources.js +145 -0
- package/src/global/intelligence/model-capability-registry.js +125 -0
- package/src/global/intelligence/model-intelligence.js +1646 -0
- package/src/global/intelligence/official-benchmark-snapshots.js +162 -0
- package/src/global/intelligence/quick-ask.js +149 -0
- package/src/global/intelligence/role-profiles.js +251 -0
- package/src/global/intelligence/skill-catalog.js +67 -0
- package/src/global/intelligence/subscription-pressure-source.js +41 -0
- package/src/global/mcp/kairo-mcp.js +51 -18
- package/src/global/mcp/work-snapshot-rule.js +4 -2
- package/src/global/mcp/workspace-binding.js +88 -0
- package/src/global/mcp/workspace-mcp-entry.js +74 -0
- package/src/global/mcp-install.js +8 -1
- package/src/global/observability/artificial-analysis-models.js +118 -0
- package/src/global/observability/claude-models.js +31 -0
- package/src/global/observability/claude-usage.js +112 -0
- package/src/global/observability/codex-models.js +96 -0
- package/src/global/observability/codex-usage.js +160 -0
- package/src/global/observability/cursor-auth.js +88 -0
- package/src/global/observability/cursor-models.js +101 -0
- package/src/global/observability/gentle-probe.js +30 -2
- package/src/global/observability/huggingface-leaderboard.js +97 -0
- package/src/global/observability/index.js +2 -1
- package/src/global/observability/opencode-models.js +101 -0
- package/src/global/observability/opencode-usage.js +162 -0
- package/src/global/paths.js +49 -2
- package/src/global/profile.js +23 -1
- package/src/global/runtime/execution-adapters/claude.js +63 -30
- package/src/global/runtime/execution-adapters/codex.js +9 -2
- package/src/global/runtime/execution-adapters/create-execution-adapter.js +6 -1
- package/src/global/runtime/execution-adapters/opencode.js +83 -18
- package/src/global/runtime/execution-worktree-manager.js +924 -0
- package/src/global/runtime/execution-worktree-orchestrator.js +194 -0
- package/src/global/runtime/execution-worktree-store.js +83 -0
- package/src/global/runtime/execution-worktree-types.js +45 -0
- package/src/global/runtime/run-events.js +38 -0
- package/src/global/runtime/run-manager.js +22 -6
- package/src/global/runtime/run-supervisor.js +41 -12
- package/src/global/runtime/usage-manager.js +96 -0
- package/src/global/runtime/usage-store.js +69 -0
- package/src/global/runtime/usage-types.js +62 -0
|
@@ -0,0 +1,456 @@
|
|
|
1
|
+
// The Complete Candidate Catalog: every real model Kairo's four provider
|
|
2
|
+
// catalogs (Codex, Claude, Cursor, OpenCode Go) actually report, mapped
|
|
3
|
+
// into one uniform ModelCandidateIdentity shape — INCLUDING models with
|
|
4
|
+
// zero real Artificial Analysis evidence (UNSCORED). This module also
|
|
5
|
+
// builds the two real consumer pools joined against it (see
|
|
6
|
+
// buildRecommendationPool/buildAutomaticExecutionPool's own docs near
|
|
7
|
+
// the bottom of this file) — deliberately SEPARATE from buildAiTeam/
|
|
8
|
+
// buildEfficientTeam's own capability-gated role ranking
|
|
9
|
+
// (model-intelligence.js), which this module never reimplements: a
|
|
10
|
+
// model with no real benchmark can still be offered for MANUAL selection
|
|
11
|
+
// here, but it must never receive an invented score — see
|
|
12
|
+
// role-profiles.js's RoleProfile.capabilities for what "scored" actually
|
|
13
|
+
// requires per role; this module never computes that, only whether AA
|
|
14
|
+
// matched the candidate at all.
|
|
15
|
+
//
|
|
16
|
+
// Per the real catalog audit this session ran against crm's live
|
|
17
|
+
// providers (codex 5, claude 9 documented, opencode-go 27, cursor 223):
|
|
18
|
+
// none of the four sources report an explicit usage multiplier anywhere
|
|
19
|
+
// — `usageMultiplier` is deliberately NOT a field here yet; adding it
|
|
20
|
+
// would mean inventing a number no provider actually gives us. Re-audit
|
|
21
|
+
// before adding it, never assume it stays true forever.
|
|
22
|
+
//
|
|
23
|
+
// lineageKey/generation are populated only for the recognized, conservative
|
|
24
|
+
// families in LINEAGE_PARSERS (see its own doc) — every other candidate
|
|
25
|
+
// gets null/null, never a guess. lifecycle is computed from THAT: a
|
|
26
|
+
// candidate is "superseded" only when a real, strictly newer generation
|
|
27
|
+
// under the exact same lineageKey is ALSO present in this same catalog
|
|
28
|
+
// (i.e. genuinely accessible, not hypothetical); everything else —
|
|
29
|
+
// unrecognized lineage, or the newest (or only) generation within a
|
|
30
|
+
// recognized one — is "current" or "unknown", never excluded from
|
|
31
|
+
// anything downstream just because its lineage couldn't be determined.
|
|
32
|
+
|
|
33
|
+
import { matchArtificialAnalysisScore } from "./model-intelligence.js";
|
|
34
|
+
|
|
35
|
+
/**
|
|
36
|
+
* @typedef {object} ModelCandidateIdentity
|
|
37
|
+
* @property {string} candidateKey - `${adapterId}::${modelId}`, this catalog's stable identity key.
|
|
38
|
+
* @property {string} modelId - the exact provider-reported id — what scoring/execution actually use, never the cleaned name.
|
|
39
|
+
* @property {string} modelName - human-readable clean name, with real effort/context/privacy variant tokens stripped (see stripDisplayVariant). Never invented — always derived from the provider's own real displayName.
|
|
40
|
+
* @property {string} rawDisplayName - the provider's own displayName, completely unmodified — the real evidence modelName was derived from. Whatever stripDisplayVariant peeled off (effort/context/privacy tokens) to produce modelName is still visible here, never a separate field: /models --evidence's own "technical detail" is just this string.
|
|
41
|
+
* @property {string} adapterId - "codex" | "claude" | "cursor" | "opencode-go".
|
|
42
|
+
* @property {"automatic"|"manual"} accessMode - whether Kairo can actually launch this candidate itself right now, or whether it's a real, recommendable option the human runs manually (Cursor always; OpenCode Go until its own empirical automatic-execution proof lands — see this module's own doc).
|
|
43
|
+
* @property {"scored"|"partial"|"unscored"} evidenceStatus - "scored": AA matched this exact model AND reports at least one of intelligenceIndex/codingIndex. "partial": AA matched it but both composite indices are null (real match, thin evidence). "unscored": no confident AA match at all. Never role-specific — see this module's own doc for why.
|
|
44
|
+
* @property {string|null} lineageKey - real, recognized model family/lineage (see LINEAGE_PARSERS) — null when the modelId doesn't match any recognized, conservative pattern. Never guessed.
|
|
45
|
+
* @property {number|null} generation - a real, comparable version number within that lineage — null whenever lineageKey is null.
|
|
46
|
+
* @property {"current"|"superseded"|"unknown"} lifecycle - "superseded" only when a real, strictly newer generation under the SAME lineageKey is also present in this catalog; "current" when it's the newest (or only) generation in a recognized lineage; "unknown" whenever lineageKey is null. An unknown lineage NEVER excludes a candidate from anything downstream.
|
|
47
|
+
* @property {{inputPerMTok: number, outputPerMTok: number}|null} resourceCost - real, provider-reported cost, when the provider actually reports one (OpenCode Go today) — never estimated or carried over from a different model.
|
|
48
|
+
*/
|
|
49
|
+
|
|
50
|
+
// Real variant tokens observed across the four live provider catalogs
|
|
51
|
+
// this session audited (Cursor's 223-model catalog especially — the only
|
|
52
|
+
// source that embeds these directly into displayName text, with no
|
|
53
|
+
// separate field). Deliberately separate from model-intelligence.js's
|
|
54
|
+
// own CONCENTRATION_SUFFIX_TOKENS (modelId-level, kebab-case) — this
|
|
55
|
+
// operates on human display TEXT (space-separated, mixed case, multi-
|
|
56
|
+
// word phrases like "Extra High") and per explicit instruction never
|
|
57
|
+
// modifies that other table. Longest phrases first so "extra high"
|
|
58
|
+
// matches before a lone trailing "high" would. Real product-tier names
|
|
59
|
+
// that happen to look like effort words in isolation — Mini, Nano,
|
|
60
|
+
// Flash, Pro, Sol, Terra, Luna, Astra, Code — are deliberately absent:
|
|
61
|
+
// they're part of a model's real identity (verified against real, very
|
|
62
|
+
// differently-priced OpenCode Go siblings, e.g. GLM-5.3 vs GLM-5.3-Flash
|
|
63
|
+
// at 1.4/4.4 vs 0.15/0.5 — genuinely different models, never a variant of
|
|
64
|
+
// each other), never stripped.
|
|
65
|
+
const VARIANT_PHRASES = ["extra high", "minimal", "medium", "thinking", "high", "low", "max", "none", "fast", "1m"];
|
|
66
|
+
const PARENTHETICAL_VARIANT = /\s*\(([^()]*)\)\s*$/;
|
|
67
|
+
|
|
68
|
+
/**
|
|
69
|
+
* Splits a provider's real displayName into a clean modelName and the
|
|
70
|
+
* real variant text stripped from its end — never touches the front of
|
|
71
|
+
* the name (a variant token is only ever a TRAILING modifier in every
|
|
72
|
+
* real example this session's audit found; a word matching the variant
|
|
73
|
+
* vocabulary elsewhere in the name is left alone). Idempotent: a name
|
|
74
|
+
* with no trailing variant tokens returns unchanged with `variant: null`.
|
|
75
|
+
* @param {string} rawDisplayName
|
|
76
|
+
* @returns {{modelName: string, variant: string|null}}
|
|
77
|
+
*/
|
|
78
|
+
export function stripDisplayVariant(rawDisplayName) {
|
|
79
|
+
const trimmed = String(rawDisplayName ?? "").trim();
|
|
80
|
+
if (!trimmed) return { modelName: trimmed, variant: null };
|
|
81
|
+
|
|
82
|
+
let working = trimmed;
|
|
83
|
+
const trailingParts = [];
|
|
84
|
+
const parenMatch = working.match(PARENTHETICAL_VARIANT);
|
|
85
|
+
if (parenMatch) {
|
|
86
|
+
working = working.slice(0, parenMatch.index).trim();
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
let tokens = working.split(/\s+/).filter(Boolean);
|
|
90
|
+
const peeled = [];
|
|
91
|
+
let peeling = true;
|
|
92
|
+
while (peeling && tokens.length) {
|
|
93
|
+
peeling = false;
|
|
94
|
+
for (const phrase of VARIANT_PHRASES) {
|
|
95
|
+
const phraseWords = phrase.split(" ");
|
|
96
|
+
if (phraseWords.length > tokens.length) continue;
|
|
97
|
+
const candidate = tokens.slice(tokens.length - phraseWords.length).join(" ").toLowerCase();
|
|
98
|
+
if (candidate === phrase) {
|
|
99
|
+
peeled.unshift(tokens.slice(tokens.length - phraseWords.length).join(" "));
|
|
100
|
+
tokens = tokens.slice(0, tokens.length - phraseWords.length);
|
|
101
|
+
peeling = true;
|
|
102
|
+
break;
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
const modelName = tokens.join(" ") || trimmed;
|
|
108
|
+
if (peeled.length) trailingParts.push(peeled.join(" "));
|
|
109
|
+
if (parenMatch) trailingParts.push(`(${parenMatch[1]})`);
|
|
110
|
+
const variant = trailingParts.length ? trailingParts.join(" ") : null;
|
|
111
|
+
return { modelName, variant };
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
// Real, conservative lineage parsers — one per recognized, unambiguous
|
|
115
|
+
// naming scheme. Each entry's `match` is deliberately STRICT (anchored
|
|
116
|
+
// start and end, no wildcard trailing segments): a modelId that doesn't
|
|
117
|
+
// match EXACTLY falls through to the next parser, and if none match, the
|
|
118
|
+
// candidate gets lineageKey/generation null — "unknown", never a guess.
|
|
119
|
+
// This strictness is itself the safety boundary: Cursor's own re-exposed
|
|
120
|
+
// ids append effort-variant suffixes this session's catalog audit found
|
|
121
|
+
// (e.g. "gpt-5.6-sol-high-fast", "claude-opus-5-thinking-high") — those
|
|
122
|
+
// intentionally DON'T match here and stay unknown, rather than trying to
|
|
123
|
+
// also parse Cursor's much larger, effort-suffixed id space in this same
|
|
124
|
+
// pass (a separate, later increment, not this one).
|
|
125
|
+
//
|
|
126
|
+
// claude-{opus|sonnet|fable|haiku}-{version}: Claude's own catalog
|
|
127
|
+
// (claude-models.js, "documented" — hand-maintained, regular naming).
|
|
128
|
+
// Version "4-8" parses as generation 4.8, "5" as 5.0 — real, verified
|
|
129
|
+
// against this session's live audit: opus-4-6 < opus-4-7 < opus-4-8 <
|
|
130
|
+
// opus-5; sonnet-4-6 < sonnet-5; fable-5 < fable-5-1.
|
|
131
|
+
//
|
|
132
|
+
// glm-{version}[-tier]: OpenCode Go's GLM models. A tier suffix (e.g.
|
|
133
|
+
// "-flash") produces a DIFFERENT lineageKey ("glm-flash") from the bare
|
|
134
|
+
// line ("glm") — verified against real, very differently-priced siblings
|
|
135
|
+
// (glm-5.3 at 1.4/4.4 vs glm-5.3-flash at 0.15/0.5 real cost): genuinely
|
|
136
|
+
// different products, never comparable generations of each other.
|
|
137
|
+
//
|
|
138
|
+
// gpt-{version}[-name]: Codex's own catalog. Same tier-suffix-changes-
|
|
139
|
+
// lineage rule as GLM — "gpt-6-astra" (lineage "gpt-astra") is never
|
|
140
|
+
// compared against "gpt-5.6-sol" (lineage "gpt-sol") or bare "gpt-5.5"
|
|
141
|
+
// (lineage "gpt") — verified against this session's audit finding no
|
|
142
|
+
// real evidence any of Codex's five current models share a persona name
|
|
143
|
+
// at two different generations yet.
|
|
144
|
+
//
|
|
145
|
+
// A real bug this same session's live-catalog verification caught before
|
|
146
|
+
// shipping: GLM/GPT's own trailing-tier group also matched Cursor's
|
|
147
|
+
// EFFORT suffixes (e.g. "gpt-5.4-low", "glm-5.2-high") — treating
|
|
148
|
+
// "-low"/"-high" as if they were real product names invented fake
|
|
149
|
+
// lineages ("gpt-low", "glm-high") that then wrongly compared DIFFERENT
|
|
150
|
+
// base generations sharing the same effort word (gpt-5.1-low vs
|
|
151
|
+
// gpt-5.2-low vs gpt-5.4-low) as if they were the same real product line,
|
|
152
|
+
// marking real, unrelated older generations "superseded" for the wrong
|
|
153
|
+
// reason. EFFORT_SUFFIX_WORDS excludes every real effort/mode token this
|
|
154
|
+
// session's audit found (mirrors model-intelligence.js's own
|
|
155
|
+
// CONCENTRATION_SUFFIX_TOKENS vocabulary, kept separate per that file's
|
|
156
|
+
// own "never modify" instruction) — a trailing word matching this set is
|
|
157
|
+
// treated as NOT a real tier name, falling through to unrecognized
|
|
158
|
+
// (null) rather than inventing a lineage split.
|
|
159
|
+
const EFFORT_SUFFIX_WORDS = new Set(["low", "medium", "high", "xhigh", "max", "none", "fast", "thinking"]);
|
|
160
|
+
const LINEAGE_PARSERS = [
|
|
161
|
+
{ pattern: /^claude-(opus|sonnet|fable|haiku)-(\d+(?:-\d+)?)$/, resolve: (m) => ({ lineageKey: `claude-${m[1]}`, generation: parseVersionToken(m[2]) }) },
|
|
162
|
+
// A real, separate id ordering this session's own live integration
|
|
163
|
+
// verification caught: Claude's documented catalog uses
|
|
164
|
+
// "claude-{tier}-{version}" (claude-sonnet-5), but Cursor's own
|
|
165
|
+
// real re-exposure of the SAME Sonnet 4/4.5/4.6 generations uses
|
|
166
|
+
// "claude-{version}-{tier}" instead (claude-4-sonnet,
|
|
167
|
+
// claude-4.6-sonnet — verified against Cursor's real catalog: no
|
|
168
|
+
// "claude-sonnet-4" id exists there at all for this specific
|
|
169
|
+
// generation, only this reordered form). Resolves to the exact same
|
|
170
|
+
// lineageKey ("claude-sonnet") as the other pattern, so a real
|
|
171
|
+
// "claude-4-sonnet" and a real "claude-sonnet-5" correctly compare
|
|
172
|
+
// as the same lineage's generations 4 and 5 — this is what actually
|
|
173
|
+
// closes the original reported bug (an old Sonnet 4 still appearing
|
|
174
|
+
// as an option once Sonnet 5 is real and accessible).
|
|
175
|
+
{ pattern: /^claude-(\d+(?:\.\d+)?)-(opus|sonnet|fable|haiku)$/, resolve: (m) => ({ lineageKey: `claude-${m[2]}`, generation: parseFloat(m[1]) }) },
|
|
176
|
+
{ pattern: /^glm-(\d+(?:\.\d+)?)(-[a-z0-9]+)?$/, resolve: (m) => resolveTieredVersion("glm", m[1], m[2]) },
|
|
177
|
+
{ pattern: /^gpt-(\d+(?:\.\d+)?)(-[a-z]+)?$/, resolve: (m) => resolveTieredVersion("gpt", m[1], m[2]) }
|
|
178
|
+
];
|
|
179
|
+
|
|
180
|
+
/** Shared by the glm/gpt parsers: a real tier suffix changes lineageKey; an effort-word suffix (Cursor's own re-exposed variants) is NOT a real tier — returns null (unrecognized) instead of inventing a fake lineage split. */
|
|
181
|
+
function resolveTieredVersion(base, versionToken, suffix) {
|
|
182
|
+
const tier = suffix ? suffix.slice(1).toLowerCase() : null;
|
|
183
|
+
if (tier && EFFORT_SUFFIX_WORDS.has(tier)) return null;
|
|
184
|
+
return { lineageKey: tier ? `${base}-${tier}` : base, generation: parseFloat(versionToken) };
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
/** Turns Claude's real hyphenated minor-version token ("4-8") into a comparable number (4.8); a bare token ("5") becomes 5. */
|
|
188
|
+
function parseVersionToken(token) {
|
|
189
|
+
return parseFloat(token.replace("-", "."));
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
// A real gap this session's own live-catalog verification found AFTER
|
|
193
|
+
// EFFORT_SUFFIX_WORDS shipped: rejecting an effort suffix at the TOP
|
|
194
|
+
// level (e.g. "claude-opus-5-thinking-high" simply failing to match the
|
|
195
|
+
// claude pattern's anchored end) avoided the false lineage, but produced
|
|
196
|
+
// a false UNKNOWN instead — a real, genuinely-superseded Cursor variant
|
|
197
|
+
// of an old Claude generation (e.g. "claude-sonnet-4-6-thinking-high")
|
|
198
|
+
// silently escaped supersession entirely, which would let it keep
|
|
199
|
+
// competing as if lineage-unknown. A variant token never changes what
|
|
200
|
+
// generation a model actually is — thinking/high/low/medium/xhigh/max/
|
|
201
|
+
// none/fast/1m and the "no-zdr" (from "(NO ZDR)") privacy marker are
|
|
202
|
+
// real, comprobados (verified against this session's own live catalog)
|
|
203
|
+
// suffixes that get stripped to compute a lineageSubjectId BEFORE
|
|
204
|
+
// matching against LINEAGE_PARSERS — modelId itself is never touched
|
|
205
|
+
// (scoring/execution/candidateKey all still use the real, exact,
|
|
206
|
+
// unmodified modelId; lineageSubjectId exists ONLY inside lineage
|
|
207
|
+
// resolution). Deliberately the SAME real vocabulary as
|
|
208
|
+
// EFFORT_SUFFIX_WORDS (kebab-case tokens here, not display-text words) —
|
|
209
|
+
// resolveTieredVersion's own EFFORT_SUFFIX_WORDS check stays as a second,
|
|
210
|
+
// defensive layer in case a future suffix token isn't in this set yet.
|
|
211
|
+
const LINEAGE_SUFFIX_TOKENS = new Set(["thinking", "high", "low", "medium", "xhigh", "max", "none", "fast", "1m"]);
|
|
212
|
+
|
|
213
|
+
/**
|
|
214
|
+
* Strips real, verified trailing variant tokens (see LINEAGE_SUFFIX_TOKENS's
|
|
215
|
+
* own doc) from a modelId to compute the id lineage resolution should
|
|
216
|
+
* actually match against — never the modelId used for scoring/execution
|
|
217
|
+
* itself. "no-zdr" (two kebab tokens) is peeled as one unit, matching
|
|
218
|
+
* displayName's own "(NO ZDR)" marker. Idempotent: an id with no
|
|
219
|
+
* recognized trailing tokens returns unchanged.
|
|
220
|
+
* @param {string} modelId
|
|
221
|
+
* @returns {string}
|
|
222
|
+
*/
|
|
223
|
+
export function stripLineageSuffixes(modelId) {
|
|
224
|
+
let tokens = String(modelId ?? "").split("-").filter(Boolean);
|
|
225
|
+
let peeling = true;
|
|
226
|
+
while (peeling && tokens.length) {
|
|
227
|
+
peeling = false;
|
|
228
|
+
if (tokens.length >= 2 && tokens[tokens.length - 2] === "no" && tokens[tokens.length - 1] === "zdr") {
|
|
229
|
+
tokens = tokens.slice(0, -2);
|
|
230
|
+
peeling = true;
|
|
231
|
+
continue;
|
|
232
|
+
}
|
|
233
|
+
if (LINEAGE_SUFFIX_TOKENS.has(tokens[tokens.length - 1])) {
|
|
234
|
+
tokens = tokens.slice(0, -1);
|
|
235
|
+
peeling = true;
|
|
236
|
+
}
|
|
237
|
+
}
|
|
238
|
+
return tokens.join("-");
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
/**
|
|
242
|
+
* Resolves a real modelId against the recognized, conservative lineage
|
|
243
|
+
* parsers above — null (never a guess) when nothing matches, even after
|
|
244
|
+
* stripping real, verified variant suffixes via stripLineageSuffixes.
|
|
245
|
+
* @param {string} modelId
|
|
246
|
+
* @returns {{lineageKey: string, generation: number}|null}
|
|
247
|
+
*/
|
|
248
|
+
export function resolveLineage(modelId) {
|
|
249
|
+
const subjectId = stripLineageSuffixes(modelId);
|
|
250
|
+
for (const { pattern, resolve } of LINEAGE_PARSERS) {
|
|
251
|
+
const match = subjectId.match(pattern);
|
|
252
|
+
if (match) return resolve(match);
|
|
253
|
+
}
|
|
254
|
+
return null;
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
/**
|
|
258
|
+
* Computes each candidate's real "current"/"superseded"/"unknown"
|
|
259
|
+
* lifecycle from lineageKey/generation already resolved onto it —
|
|
260
|
+
* "superseded" only when a real, strictly newer generation under the
|
|
261
|
+
* SAME lineageKey is ALSO present in `catalog` (i.e. genuinely
|
|
262
|
+
* accessible right now, not merely a known future release). Returns a
|
|
263
|
+
* NEW array (candidates are copied, never mutated in place).
|
|
264
|
+
* @param {Array<ModelCandidateIdentity>} catalog
|
|
265
|
+
* @returns {Array<ModelCandidateIdentity>}
|
|
266
|
+
*/
|
|
267
|
+
function applyLifecycle(catalog) {
|
|
268
|
+
const maxGenerationByLineage = new Map();
|
|
269
|
+
for (const candidate of catalog) {
|
|
270
|
+
if (candidate.lineageKey == null) continue;
|
|
271
|
+
const current = maxGenerationByLineage.get(candidate.lineageKey);
|
|
272
|
+
if (current == null || candidate.generation > current) maxGenerationByLineage.set(candidate.lineageKey, candidate.generation);
|
|
273
|
+
}
|
|
274
|
+
return catalog.map((candidate) => {
|
|
275
|
+
if (candidate.lineageKey == null) return { ...candidate, lifecycle: "unknown" };
|
|
276
|
+
const max = maxGenerationByLineage.get(candidate.lineageKey);
|
|
277
|
+
return { ...candidate, lifecycle: candidate.generation < max ? "superseded" : "current" };
|
|
278
|
+
});
|
|
279
|
+
}
|
|
280
|
+
|
|
281
|
+
// Whether Kairo can actually launch a candidate itself right now, per
|
|
282
|
+
// adapter — real, current state (execution-adapters/index.js's own
|
|
283
|
+
// `launchable` flags, intelligence/execution-router.js's checkCandidate),
|
|
284
|
+
// not a guess. OpenCode Go stays "manual" until the real empirical
|
|
285
|
+
// automatic-execution proof this session's plan calls for
|
|
286
|
+
// (`opencode run -m opencode-go/<model>`, verifying real provider
|
|
287
|
+
// attribution and Go-only consumption) actually runs and passes — see
|
|
288
|
+
// this module's own doc. Cursor is permanently "manual" by design (see
|
|
289
|
+
// execution-router.js's own checkCandidate: real task execution always
|
|
290
|
+
// refuses it, recommendation never does).
|
|
291
|
+
const ACCESS_MODE_BY_ADAPTER = { codex: "automatic", claude: "automatic", cursor: "manual", "opencode-go": "manual" };
|
|
292
|
+
|
|
293
|
+
function resolveAccessMode(adapterId, modelId) {
|
|
294
|
+
if (adapterId === "cursor" && modelId === "auto") return "manual";
|
|
295
|
+
return ACCESS_MODE_BY_ADAPTER[adapterId] ?? "manual";
|
|
296
|
+
}
|
|
297
|
+
|
|
298
|
+
function resolveEvidenceStatus(matched) {
|
|
299
|
+
if (!matched) return "unscored";
|
|
300
|
+
return matched.intelligenceIndex == null && matched.codingIndex == null ? "partial" : "scored";
|
|
301
|
+
}
|
|
302
|
+
|
|
303
|
+
function resolveResourceCost(rawModel) {
|
|
304
|
+
const input = rawModel?.costInputPerMTok;
|
|
305
|
+
const output = rawModel?.costOutputPerMTok;
|
|
306
|
+
return typeof input === "number" && typeof output === "number" ? { inputPerMTok: input, outputPerMTok: output } : null;
|
|
307
|
+
}
|
|
308
|
+
|
|
309
|
+
/**
|
|
310
|
+
* Builds one ModelCandidateIdentity for a single real provider model —
|
|
311
|
+
* the Cursor "auto" router gets an honest, deliberately opaque identity
|
|
312
|
+
* (never an assumed inner model): modelName "Cursor Auto", no variant,
|
|
313
|
+
* always unscored/manual/unknown-lifecycle.
|
|
314
|
+
* @param {string} adapterId
|
|
315
|
+
* @param {{id: string, displayName?: string}} rawModel
|
|
316
|
+
* @param {Array<object>} aaModels
|
|
317
|
+
* @param {(modelId: string, aaModels: Array<object>) => object|null} matcher
|
|
318
|
+
* @returns {ModelCandidateIdentity}
|
|
319
|
+
*/
|
|
320
|
+
function buildCandidateIdentity(adapterId, rawModel, aaModels, matcher) {
|
|
321
|
+
const modelId = rawModel.id;
|
|
322
|
+
const rawDisplayName = rawModel.displayName ?? modelId;
|
|
323
|
+
|
|
324
|
+
if (adapterId === "cursor" && modelId === "auto") {
|
|
325
|
+
return {
|
|
326
|
+
candidateKey: `${adapterId}::${modelId}`, modelId, modelName: "Cursor Auto", rawDisplayName,
|
|
327
|
+
adapterId, accessMode: "manual", evidenceStatus: "unscored",
|
|
328
|
+
lineageKey: null, generation: null, lifecycle: "unknown", resourceCost: null
|
|
329
|
+
};
|
|
330
|
+
}
|
|
331
|
+
|
|
332
|
+
const { modelName } = stripDisplayVariant(rawDisplayName);
|
|
333
|
+
const matched = matcher(modelId, aaModels);
|
|
334
|
+
const lineage = resolveLineage(modelId);
|
|
335
|
+
return {
|
|
336
|
+
candidateKey: `${adapterId}::${modelId}`, modelId, modelName, rawDisplayName,
|
|
337
|
+
adapterId, accessMode: resolveAccessMode(adapterId, modelId), evidenceStatus: resolveEvidenceStatus(matched),
|
|
338
|
+
// lifecycle is resolved in a second pass (applyLifecycle, called from
|
|
339
|
+
// buildCompleteCandidateCatalog) once the WHOLE catalog is known —
|
|
340
|
+
// "superseded" is a statement about this candidate relative to its
|
|
341
|
+
// real siblings, not something a single candidate can determine
|
|
342
|
+
// alone. "unknown" here is only a placeholder for lineage == null;
|
|
343
|
+
// applyLifecycle overwrites it for every recognized lineage.
|
|
344
|
+
lineageKey: lineage?.lineageKey ?? null, generation: lineage?.generation ?? null, lifecycle: "unknown",
|
|
345
|
+
resourceCost: resolveResourceCost(rawModel)
|
|
346
|
+
};
|
|
347
|
+
}
|
|
348
|
+
|
|
349
|
+
/**
|
|
350
|
+
* The Complete Candidate Catalog: every real model across every real
|
|
351
|
+
* provider catalog given, mapped to ModelCandidateIdentity — unfiltered,
|
|
352
|
+
* UNSCORED candidates included. Never the ranking surface (see this
|
|
353
|
+
* module's own doc) — just the real, complete inventory.
|
|
354
|
+
* @param {Array<{adapterId: string, models: Array<{id: string, displayName?: string}>}>} providerCatalogs -
|
|
355
|
+
* same shape model-intelligence.js's scoreAvailableModels takes, e.g.
|
|
356
|
+
* `[{ adapterId: "codex", models: readCodexModels().models }, ...]`.
|
|
357
|
+
* @param {Array<object>} aaModels - readArtificialAnalysisModels().models
|
|
358
|
+
* @param {{matchArtificialAnalysisScore?: (modelId: string, aaModels: Array<object>) => object|null}} [deps] -
|
|
359
|
+
* injectable for tests; defaults to model-intelligence.js's real export.
|
|
360
|
+
* @returns {Array<ModelCandidateIdentity>}
|
|
361
|
+
*/
|
|
362
|
+
export function buildCompleteCandidateCatalog(providerCatalogs, aaModels, deps = {}) {
|
|
363
|
+
const matcher = deps.matchArtificialAnalysisScore ?? matchArtificialAnalysisScore;
|
|
364
|
+
const catalog = [];
|
|
365
|
+
for (const { adapterId, models } of providerCatalogs) {
|
|
366
|
+
for (const rawModel of models ?? []) {
|
|
367
|
+
catalog.push(buildCandidateIdentity(adapterId, rawModel, aaModels, matcher));
|
|
368
|
+
}
|
|
369
|
+
}
|
|
370
|
+
return applyLifecycle(catalog);
|
|
371
|
+
}
|
|
372
|
+
|
|
373
|
+
/**
|
|
374
|
+
* @typedef {object} RecommendationPoolCandidate
|
|
375
|
+
* One real scoreAvailableModels() entry (adapterId, modelId,
|
|
376
|
+
* intelligenceIndex, codingIndex, priceInputPerMTok, etc. — completely
|
|
377
|
+
* untouched, never recomputed), spread together with: candidateKey,
|
|
378
|
+
* modelName, accessMode, evidenceStatus, lineageKey, generation,
|
|
379
|
+
* lifecycle, resourceCost from the joined ModelCandidateIdentity.
|
|
380
|
+
* `resourceCost` and `priceInputPerMTok` are DELIBERATELY separate
|
|
381
|
+
* fields — OpenCode Go's own real per-model cost is never blended into
|
|
382
|
+
* or mistaken for AA's price figure, even when both exist for the same
|
|
383
|
+
* candidate.
|
|
384
|
+
*/
|
|
385
|
+
|
|
386
|
+
/**
|
|
387
|
+
* The Recommendation Pool: every real, scored candidate (from
|
|
388
|
+
* scoreAvailableModels — the same real AA-matched pool buildAiTeam/
|
|
389
|
+
* buildEfficientTeam already consume) that is NOT superseded, enriched
|
|
390
|
+
* with its real ModelCandidateIdentity fields via a candidateKey join.
|
|
391
|
+
* Deliberately does NOT recompute capability/role fit itself — that
|
|
392
|
+
* stays model-intelligence.js's own job; a caller feeds this pool's
|
|
393
|
+
* output straight into buildAiTeam/buildEfficientTeam/
|
|
394
|
+
* bestModelPerRoleGlobal/etc. as their own `models` argument, in place
|
|
395
|
+
* of raw scoreAvailableModels() output, so QUALITY/EFFICIENT TEAM
|
|
396
|
+
* naturally stop considering a real, superseded old generation without
|
|
397
|
+
* either team's own ranking logic needing to know why.
|
|
398
|
+
*
|
|
399
|
+
* "current" and "unknown" lifecycle candidates are BOTH kept — an
|
|
400
|
+
* unrecognized lineage never excludes a real candidate from being
|
|
401
|
+
* recommended, only a PROVEN newer same-lineage successor does. Manual-
|
|
402
|
+
* only real candidates (Cursor, OpenCode Go today) are kept too — this
|
|
403
|
+
* pool answers "what can Kairo honestly recommend", not "what can Kairo
|
|
404
|
+
* launch by itself" (see buildAutomaticExecutionPool for that).
|
|
405
|
+
* @param {Array<object>} scoredAll - scoreAvailableModels() output, every candidate provider regardless of eligibility.
|
|
406
|
+
* @param {Array<ModelCandidateIdentity>} completeCatalog - buildCompleteCandidateCatalog() output, same provider catalogs.
|
|
407
|
+
* @returns {Array<RecommendationPoolCandidate>}
|
|
408
|
+
*/
|
|
409
|
+
export function buildRecommendationPool(scoredAll, completeCatalog) {
|
|
410
|
+
const identityByKey = new Map(completeCatalog.map((identity) => [identity.candidateKey, identity]));
|
|
411
|
+
const pool = [];
|
|
412
|
+
for (const scored of scoredAll) {
|
|
413
|
+
const candidateKey = `${scored.adapterId}::${scored.modelId}`;
|
|
414
|
+
const identity = identityByKey.get(candidateKey);
|
|
415
|
+
// No real catalog identity for this exact scored candidate is only
|
|
416
|
+
// possible if the caller passed mismatched provider catalogs to
|
|
417
|
+
// scoreAvailableModels vs buildCompleteCandidateCatalog — defensive,
|
|
418
|
+
// never excludes: an un-joined candidate is treated as lineage-
|
|
419
|
+
// unknown, exactly like any other real unrecognized lineage.
|
|
420
|
+
if (identity?.lifecycle === "superseded") continue;
|
|
421
|
+
pool.push({
|
|
422
|
+
...scored,
|
|
423
|
+
candidateKey,
|
|
424
|
+
modelName: identity?.modelName ?? scored.displayName ?? scored.modelId,
|
|
425
|
+
accessMode: identity?.accessMode ?? "manual",
|
|
426
|
+
evidenceStatus: identity?.evidenceStatus ?? "scored",
|
|
427
|
+
lineageKey: identity?.lineageKey ?? null,
|
|
428
|
+
generation: identity?.generation ?? null,
|
|
429
|
+
lifecycle: identity?.lifecycle ?? "unknown",
|
|
430
|
+
resourceCost: identity?.resourceCost ?? null
|
|
431
|
+
});
|
|
432
|
+
}
|
|
433
|
+
return pool;
|
|
434
|
+
}
|
|
435
|
+
|
|
436
|
+
/**
|
|
437
|
+
* The Automatic Execution Pool: the subset of the Recommendation Pool
|
|
438
|
+
* Kairo can actually launch itself, right now — real routing's own
|
|
439
|
+
* candidate source, never QUALITY/EFFICIENT TEAM's. Requires BOTH a real
|
|
440
|
+
* accessMode of "automatic" (Cursor/OpenCode Go are permanently or
|
|
441
|
+
* currently manual — see ModelCandidateIdentity's own doc) AND real,
|
|
442
|
+
* current eligibility (adapter availability, quota, launchability — the
|
|
443
|
+
* exact same `eligibility` object checkCandidate/execution-router.js
|
|
444
|
+
* already compute, reused here rather than reimplemented). Never
|
|
445
|
+
* mutates or filters the Recommendation Pool itself — a manual-only real
|
|
446
|
+
* recommendation (Cursor, say) stays fully visible there; a caller that
|
|
447
|
+
* wants to actually RUN a task must separately produce a real
|
|
448
|
+
* "Continue in Cursor"-style handoff for it, never a silent fallback to
|
|
449
|
+
* a different, automatically-launchable model the human didn't ask for.
|
|
450
|
+
* @param {Array<RecommendationPoolCandidate>} recommendationPool
|
|
451
|
+
* @param {Record<string, {ok: boolean, reason?: string}>} eligibility - checkCandidate() results per adapterId.
|
|
452
|
+
* @returns {Array<RecommendationPoolCandidate>}
|
|
453
|
+
*/
|
|
454
|
+
export function buildAutomaticExecutionPool(recommendationPool, eligibility) {
|
|
455
|
+
return recommendationPool.filter((candidate) => candidate.accessMode === "automatic" && eligibility[candidate.adapterId]?.ok === true);
|
|
456
|
+
}
|
|
@@ -0,0 +1,145 @@
|
|
|
1
|
+
// Feeds real evidence into a CapabilityRegistry (model-capability-registry.js).
|
|
2
|
+
// Two sources so far: Artificial Analysis (already integrated elsewhere in
|
|
3
|
+
// Kairo) and Hugging Face's per-benchmark leaderboard API, scoped to
|
|
4
|
+
// OpenCode Go's open-weight models only — verified live that HF's
|
|
5
|
+
// leaderboards have ZERO coverage of Codex/Claude (proprietary, not hosted
|
|
6
|
+
// on the HF Hub) and real coverage of Go's model family (GLM/Kimi/DeepSeek/
|
|
7
|
+
// Qwen/MiniMax, all HF-hosted). Adding another source (Epoch AI, OpenRouter
|
|
8
|
+
// metadata) means adding another `ingest*` function here, never changing
|
|
9
|
+
// the registry itself.
|
|
10
|
+
|
|
11
|
+
import { matchArtificialAnalysisScore } from "./model-intelligence.js";
|
|
12
|
+
|
|
13
|
+
// One evidence entry per real, non-null field AA reports — never a
|
|
14
|
+
// fabricated zero for a metric AA doesn't have for that model. The
|
|
15
|
+
// per-benchmark fields (gpqa, hle, ...) are real scores the free API
|
|
16
|
+
// already returns alongside the composite indices — verified live, an
|
|
17
|
+
// earlier assumption that free tier lacked them was wrong.
|
|
18
|
+
const AA_METRICS = [
|
|
19
|
+
"intelligenceIndex", "codingIndex", "mathIndex", "priceInputPerMTok", "outputTokensPerSecond",
|
|
20
|
+
"gpqa", "hle", "sciCode", "mmluPro", "liveCodeBench", "ifBench", "terminalBenchHard", "terminalBenchV2", "tau2", "tauBanking"
|
|
21
|
+
];
|
|
22
|
+
|
|
23
|
+
// The REAL scale each AA metric is reported on — declared here, not
|
|
24
|
+
// guessed downstream by capability-scoring.js's metric-name-based
|
|
25
|
+
// fallback. AA's own per-benchmark fields (gpqa/hle/sciCode/mmluPro/
|
|
26
|
+
// liveCodeBench/ifBench/terminalBenchHard/terminalBenchV2/tau2/
|
|
27
|
+
// tauBanking) are real 0-1 fractions. intelligenceIndex/codingIndex/
|
|
28
|
+
// mathIndex are AA's own 0-100 composite indices. price/speed aren't
|
|
29
|
+
// percentage-like values at all — no real "unit vs hundred" question
|
|
30
|
+
// applies, left undeclared (undefined here means addEvidence gets no
|
|
31
|
+
// scale for them, same as before this fix).
|
|
32
|
+
const AA_METRIC_SCALE = {
|
|
33
|
+
gpqa: "unit", hle: "unit", sciCode: "unit", mmluPro: "unit", liveCodeBench: "unit", ifBench: "unit",
|
|
34
|
+
terminalBenchHard: "unit", terminalBenchV2: "unit", tau2: "unit", tauBanking: "unit",
|
|
35
|
+
intelligenceIndex: "hundred", codingIndex: "hundred", mathIndex: "hundred"
|
|
36
|
+
};
|
|
37
|
+
|
|
38
|
+
/**
|
|
39
|
+
* Registers every real, matched (provider catalog ↔ Artificial Analysis)
|
|
40
|
+
* model as an identity, and records one evidence entry per real metric it
|
|
41
|
+
* reports. AA's free tier never independently verifies a result the way a
|
|
42
|
+
* leaderboard's per-submission `verified` flag would, so every entry here
|
|
43
|
+
* is honestly `verified: false` — that distinction matters once a source
|
|
44
|
+
* that does verify (e.g. Hugging Face leaderboards) is added.
|
|
45
|
+
* @param {ReturnType<import("./capability-registry.js").createCapabilityRegistry>} registry
|
|
46
|
+
* @param {Array<{adapterId: string, models: Array<object|string>}>} providerCatalogs
|
|
47
|
+
* @param {Array<object>} aaModels
|
|
48
|
+
* @param {{fetchedAt?: string}} [options]
|
|
49
|
+
*/
|
|
50
|
+
export function ingestArtificialAnalysisEvidence(registry, providerCatalogs, aaModels, { fetchedAt = new Date().toISOString() } = {}) {
|
|
51
|
+
for (const { adapterId, models } of providerCatalogs) {
|
|
52
|
+
for (const entry of models ?? []) {
|
|
53
|
+
const model = typeof entry === "string" ? { id: entry, displayName: entry } : entry;
|
|
54
|
+
const match = matchArtificialAnalysisScore(model.id, aaModels);
|
|
55
|
+
if (!match) continue;
|
|
56
|
+
const id = registry.registerIdentity(adapterId, model.id, model.displayName ?? null);
|
|
57
|
+
for (const metric of AA_METRICS) {
|
|
58
|
+
const value = match[metric];
|
|
59
|
+
if (value == null) continue;
|
|
60
|
+
registry.addEvidence(id, {
|
|
61
|
+
metric, value, source: "artificial-analysis-free", scale: AA_METRIC_SCALE[metric] ?? null,
|
|
62
|
+
benchmarkVersion: null, modelConfig: null, date: fetchedAt, verified: false
|
|
63
|
+
});
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
return registry;
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
function normalizeHfId(id) {
|
|
71
|
+
return String(id ?? "").toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-+|-+$/g, "");
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
function sortedTokens(normalizedId) {
|
|
75
|
+
return normalizedId.split("-").filter(Boolean).sort().join("-");
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
/**
|
|
79
|
+
* Finds every real leaderboard entry for a model id — plural, never just
|
|
80
|
+
* the first hit, because the same model can legitimately appear more than
|
|
81
|
+
* once in one dataset with different real configs/harnesses (e.g. "with
|
|
82
|
+
* tools" vs. not) and those aren't the same measurement. HF's `modelId` is
|
|
83
|
+
* "org/model" (e.g. "zai-org/GLM-5.3"); only the model part is compared,
|
|
84
|
+
* since Kairo's own catalog ids never carry an org prefix.
|
|
85
|
+
* @param {string} modelId
|
|
86
|
+
* @param {Array<{modelId: string}>} hfEntries
|
|
87
|
+
*/
|
|
88
|
+
export function matchHuggingFaceEntries(modelId, hfEntries) {
|
|
89
|
+
const normalized = normalizeHfId(modelId);
|
|
90
|
+
if (!normalized) return [];
|
|
91
|
+
const withModelPart = hfEntries.map((entry) => {
|
|
92
|
+
const raw = String(entry.modelId ?? "");
|
|
93
|
+
const modelPart = raw.includes("/") ? raw.slice(raw.lastIndexOf("/") + 1) : raw;
|
|
94
|
+
return { entry, normalized: normalizeHfId(modelPart) };
|
|
95
|
+
});
|
|
96
|
+
const exact = withModelPart.filter((c) => c.normalized === normalized);
|
|
97
|
+
if (exact.length) return exact.map((c) => c.entry);
|
|
98
|
+
const tokens = sortedTokens(normalized);
|
|
99
|
+
return withModelPart.filter((c) => sortedTokens(c.normalized) === tokens).map((c) => c.entry);
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
/**
|
|
103
|
+
* Registers real leaderboard evidence for one benchmark (one HF dataset =
|
|
104
|
+
* one benchmark — there's no cross-benchmark aggregate endpoint). Every
|
|
105
|
+
* real match becomes its own evidence entry (never averaged, never
|
|
106
|
+
* deduplicated to "the first one") so different real configs/harnesses for
|
|
107
|
+
* the same model stay distinguishable, exactly like the registry's own
|
|
108
|
+
* contract requires. `verified` is real per-entry data from HF (an
|
|
109
|
+
* independent-verification flag on that specific submission), unlike AA's
|
|
110
|
+
* free tier which never verifies anything.
|
|
111
|
+
* @param {ReturnType<import("./model-capability-registry.js").createCapabilityRegistry>} registry
|
|
112
|
+
* @param {Array<{adapterId: string, models: Array<object|string>}>} providerCatalogs
|
|
113
|
+
* @param {Array<{modelId: string, value: number|null, verified: boolean, notes?: string|null}>} hfEntries
|
|
114
|
+
* @param {{metric: string, scale?: "unit"|"hundred", benchmarkVersion?: string|null, fetchedAt?: string}} options -
|
|
115
|
+
* `scale` is the REAL scale this exact HF dataset reports on — the
|
|
116
|
+
* caller's own responsibility to know, since a different source can
|
|
117
|
+
* report the SAME metric name in a genuinely different real scale (see
|
|
118
|
+
* the real bug this parameter fixes: HLE via Hugging Face reports 0-100,
|
|
119
|
+
* verified live — DeepSeek-V4.1-Flash's real value is 63.9, not 0.639 —
|
|
120
|
+
* while Artificial Analysis's own "hle" field is a real 0-1 fraction;
|
|
121
|
+
* both used to share capability-scoring.js's single metric-name-based
|
|
122
|
+
* "unit" assumption, silently treating HF's 63.9 as if it were already
|
|
123
|
+
* a fraction). Omit only when genuinely unknown — capability-scoring.js
|
|
124
|
+
* falls back to its own metric-name guess, which is exactly the
|
|
125
|
+
* behavior that produced the bug.
|
|
126
|
+
*/
|
|
127
|
+
export function ingestHuggingFaceLeaderboardEvidence(registry, providerCatalogs, hfEntries, { metric, scale = null, benchmarkVersion = null, fetchedAt = new Date().toISOString() }) {
|
|
128
|
+
for (const { adapterId, models } of providerCatalogs) {
|
|
129
|
+
for (const entry of models ?? []) {
|
|
130
|
+
const model = typeof entry === "string" ? { id: entry, displayName: entry } : entry;
|
|
131
|
+
const matches = matchHuggingFaceEntries(model.id, hfEntries);
|
|
132
|
+
if (!matches.length) continue;
|
|
133
|
+
const id = registry.registerIdentity(adapterId, model.id, model.displayName ?? null);
|
|
134
|
+
for (const match of matches) {
|
|
135
|
+
if (match.value == null) continue;
|
|
136
|
+
registry.addEvidence(id, {
|
|
137
|
+
metric, value: match.value, source: "huggingface-leaderboard", scale,
|
|
138
|
+
benchmarkVersion, modelConfig: match.notes ?? match.modelId ?? null,
|
|
139
|
+
date: fetchedAt, verified: match.verified === true
|
|
140
|
+
});
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
return registry;
|
|
145
|
+
}
|