@flame0510/project-aether 1.6.0 → 1.6.2
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/app/api/update-check/route.ts +4 -1
- package/app/components/VersionBanner.tsx +80 -52
- package/docs/dev/API-REFERENCE.md +4 -0
- package/docs/dev/GATEWAY.md +23 -0
- package/models.config.json +937 -45
- package/package.json +4 -2
- package/scripts/model-info-verify.mjs +194 -0
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@flame0510/project-aether",
|
|
3
|
-
"version": "1.6.
|
|
3
|
+
"version": "1.6.2",
|
|
4
4
|
"description": "Rev4a — Revolution for Agents. OpenClaw agent fleet orchestrator.",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"openclaw",
|
|
@@ -57,7 +57,9 @@
|
|
|
57
57
|
"refresh:pricing": "node scripts/refresh-model-pricing.mjs",
|
|
58
58
|
"smoke:db-bootstrap": "node scripts/smoke-db-bootstrap.mjs",
|
|
59
59
|
"postinstall": "node bin/postinstall.js",
|
|
60
|
-
"check:language": "node scripts/check-language.mjs"
|
|
60
|
+
"check:language": "node scripts/check-language.mjs",
|
|
61
|
+
"info:suggest": "node scripts/model-info-suggest.mjs",
|
|
62
|
+
"info:verify": "node scripts/model-info-verify.mjs"
|
|
61
63
|
},
|
|
62
64
|
"dependencies": {
|
|
63
65
|
"@types/better-sqlite3": "^7.6.13",
|
|
@@ -0,0 +1,194 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/**
|
|
3
|
+
* Verifies proposed `info` blocks against the model cards they cite — the gate between a
|
|
4
|
+
* curator agent's proposal and `models.config.json`. Nothing is written: the script says
|
|
5
|
+
* which proposals hold and which do not.
|
|
6
|
+
*
|
|
7
|
+
* What it checks, per model:
|
|
8
|
+
* - the id exists in the catalogue, and the model has a Hugging Face card;
|
|
9
|
+
* - `evidence` is present (a proposal without it is not verifiable);
|
|
10
|
+
* - every number in `params` appears in the card text;
|
|
11
|
+
* - every benchmark value sits in a row named after the benchmark, under a column whose
|
|
12
|
+
* header names this model (or its instruct variant) — the nearest header row that
|
|
13
|
+
* carries the model's name, so a card with a stacked or merged header still resolves;
|
|
14
|
+
* - `docUrl`, when asked with `--check-links`, answers 200.
|
|
15
|
+
*
|
|
16
|
+
* Usage:
|
|
17
|
+
* node scripts/model-info-verify.mjs <file with the proposals|-> [--check-links]
|
|
18
|
+
* The file may be raw agent output: JSON objects are extracted from it.
|
|
19
|
+
*/
|
|
20
|
+
import * as fs from 'node:fs';
|
|
21
|
+
import * as path from 'node:path';
|
|
22
|
+
import { fileURLToPath } from 'node:url';
|
|
23
|
+
|
|
24
|
+
const ROOT = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '..');
|
|
25
|
+
const args = process.argv.slice(2);
|
|
26
|
+
const input = args.find((a) => !a.startsWith('--')) ?? '-';
|
|
27
|
+
const checkLinks = args.includes('--check-links');
|
|
28
|
+
|
|
29
|
+
const catalogue = new Set(JSON.parse(fs.readFileSync(path.join(ROOT, 'models.config.json'), 'utf-8')).models.map((m) => m.id));
|
|
30
|
+
const details = (() => {
|
|
31
|
+
try { return JSON.parse(fs.readFileSync(path.join(ROOT, 'model-details.json'), 'utf-8')).models ?? {}; } catch { return {}; }
|
|
32
|
+
})();
|
|
33
|
+
|
|
34
|
+
const decode = (s) => s.replace(/<[^>]*>/g, ' ').replace(/</g, '<').replace(/>/g, '>').replace(/&/g, '&').replace(/"/g, '"').replace(/'/g, "'").replace(/\s+/g, ' ').trim();
|
|
35
|
+
const norm = (s) => s.toLowerCase().replace(/[^a-z0-9]/g, '');
|
|
36
|
+
|
|
37
|
+
/** Every table row of a card, markdown and HTML, as normalised `| a | b |` strings. */
|
|
38
|
+
function tableRows(md) {
|
|
39
|
+
const rows = md.split('\n').filter((l) => l.trim().startsWith('|')).map(decode);
|
|
40
|
+
for (const tr of md.matchAll(/<tr[^>]*>([\s\S]*?)<\/tr>/gi)) {
|
|
41
|
+
const cells = [...tr[1].matchAll(/<t[dh][^>]*>([\s\S]*?)<\/t[dh]>/gi)].map((c) => decode(c[1]));
|
|
42
|
+
if (cells.length) rows.push(`| ${cells.join(' | ')} |`);
|
|
43
|
+
}
|
|
44
|
+
return rows;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
const cellsOf = (row) => row.split('|').slice(1, -1).map((c) => c.replace(/\*/g, '').trim());
|
|
48
|
+
|
|
49
|
+
/**
|
|
50
|
+
* A header cell names the model when it carries the same distinctive run of characters:
|
|
51
|
+
* cards label columns with a display name ("Kimi K2.5 (Thinking)", "Nemotron-3-Super")
|
|
52
|
+
* that is not the id, and an exact match would miss every one of them.
|
|
53
|
+
*/
|
|
54
|
+
function namesModel(cell, modelNames) {
|
|
55
|
+
const c = norm(cell);
|
|
56
|
+
if (!c) return false;
|
|
57
|
+
return modelNames.some((n) => {
|
|
58
|
+
const m = norm(n);
|
|
59
|
+
if (!m) return false;
|
|
60
|
+
for (let len = Math.min(c.length, m.length); len >= 6; len--) {
|
|
61
|
+
for (let i = 0; i + len <= c.length; i++) if (m.includes(c.slice(i, i + len))) return true;
|
|
62
|
+
}
|
|
63
|
+
return false;
|
|
64
|
+
});
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
/** The nearest row above `row` that names the model: the header this value belongs under. */
|
|
68
|
+
function headerNaming(rows, rowIndex, modelNames) {
|
|
69
|
+
for (let i = rowIndex - 1; i >= 0; i--) {
|
|
70
|
+
if (!rows[i]) break;
|
|
71
|
+
const cells = cellsOf(rows[i]);
|
|
72
|
+
if (cells.some((c) => namesModel(c, modelNames))) return cells;
|
|
73
|
+
}
|
|
74
|
+
return null;
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
function proposalObjects(text) {
|
|
78
|
+
const out = [];
|
|
79
|
+
for (let i = 0; i < text.length; i++) {
|
|
80
|
+
if (text[i] !== '{') continue;
|
|
81
|
+
let depth = 0, inString = false, escaped = false, end = -1;
|
|
82
|
+
for (let j = i; j < text.length; j++) {
|
|
83
|
+
const ch = text[j];
|
|
84
|
+
if (inString) {
|
|
85
|
+
if (escaped) escaped = false;
|
|
86
|
+
else if (ch === '\\') escaped = true;
|
|
87
|
+
else if (ch === '"') inString = false;
|
|
88
|
+
continue;
|
|
89
|
+
}
|
|
90
|
+
if (ch === '"') inString = true;
|
|
91
|
+
else if (ch === '{') depth++;
|
|
92
|
+
else if (ch === '}') { depth--; if (depth === 0) { end = j; break; } }
|
|
93
|
+
}
|
|
94
|
+
if (end < 0) continue; // an unclosed fragment (engine logs) must not stop the scan
|
|
95
|
+
try {
|
|
96
|
+
const parsed = JSON.parse(text.slice(i, end + 1));
|
|
97
|
+
if (parsed && parsed.id && (parsed.info || parsed.skipped)) out.push(parsed);
|
|
98
|
+
} catch { /* not a proposal object */ }
|
|
99
|
+
i = end;
|
|
100
|
+
}
|
|
101
|
+
return out;
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
async function cardFor(hfId) {
|
|
105
|
+
const cached = path.join('/tmp', `card-${hfId.replace('/', '_')}.md`);
|
|
106
|
+
if (fs.existsSync(cached)) return fs.readFileSync(cached, 'utf-8');
|
|
107
|
+
const res = await fetch(`https://huggingface.co/${hfId}/raw/main/README.md`, { signal: AbortSignal.timeout(30_000) });
|
|
108
|
+
if (!res.ok) throw new Error(`card HTTP ${res.status}`);
|
|
109
|
+
const md = await res.text();
|
|
110
|
+
fs.writeFileSync(cached, md);
|
|
111
|
+
return md;
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
async function verify(model, catalogueId) {
|
|
115
|
+
const problems = [];
|
|
116
|
+
const warnings = [];
|
|
117
|
+
if (!catalogue.has(catalogueId)) problems.push('id not in the catalogue');
|
|
118
|
+
if (model.skipped) return { problems, warnings: [`skipped: ${model.skipped}`] };
|
|
119
|
+
const hfId = details[catalogueId]?.hf?.id;
|
|
120
|
+
if (!hfId) return { problems, warnings: ['no Hugging Face card: not verifiable'] };
|
|
121
|
+
if (!model.evidence?.length) problems.push('evidence missing');
|
|
122
|
+
let md;
|
|
123
|
+
try {
|
|
124
|
+
md = await cardFor(hfId);
|
|
125
|
+
} catch (e) {
|
|
126
|
+
if (e instanceof Error) return { problems, warnings: [`card unavailable: ${e.message}`] };
|
|
127
|
+
return { problems, warnings: ['card unavailable'] };
|
|
128
|
+
}
|
|
129
|
+
const rows = tableRows(md);
|
|
130
|
+
const shortName = hfId.split('/').pop();
|
|
131
|
+
const modelNames = [shortName, catalogueId.split('/').pop(), shortName.replace(/-/g, '')];
|
|
132
|
+
|
|
133
|
+
const info = model.info ?? {};
|
|
134
|
+
if (info.params) {
|
|
135
|
+
const numbers = info.params.match(/\d+(?:\.\d+)?\s*[BT]/g) ?? [];
|
|
136
|
+
const missing = numbers.filter((n) => !md.replace(/\s/g, '').includes(n.replace(/\s/g, '')));
|
|
137
|
+
if (missing.length) problems.push(`params: numbers not in the card (${missing.join(', ')})`);
|
|
138
|
+
}
|
|
139
|
+
if (info.docUrl && checkLinks) {
|
|
140
|
+
try {
|
|
141
|
+
const res = await fetch(info.docUrl, { method: 'GET', signal: AbortSignal.timeout(20_000) });
|
|
142
|
+
if (!res.ok) problems.push(`docUrl: HTTP ${res.status}`);
|
|
143
|
+
} catch {
|
|
144
|
+
problems.push('docUrl: unreachable');
|
|
145
|
+
}
|
|
146
|
+
}
|
|
147
|
+
const sameNumber = (cell, value) => {
|
|
148
|
+
const parsed = Number(String(cell).replace(/[^0-9.\-]/g, ''));
|
|
149
|
+
return Number.isFinite(parsed) && Math.abs(parsed - value) < 1e-9;
|
|
150
|
+
};
|
|
151
|
+
for (const bench of info.benchmarks ?? []) {
|
|
152
|
+
// A benchmark name can appear in more than one table (or in prose rows); accept the
|
|
153
|
+
// value when any row named after it carries it, then check the column of that row.
|
|
154
|
+
const candidates = rows
|
|
155
|
+
.map((r, index) => ({ r, index }))
|
|
156
|
+
.filter(({ r }) => r.toLowerCase().includes(String(bench.name).toLowerCase()) && /\d/.test(r));
|
|
157
|
+
if (candidates.length === 0) { problems.push(`${bench.name}: no matching row`); continue; }
|
|
158
|
+
const hit = candidates.find(({ r }) => cellsOf(r).some((c) => sameNumber(c, bench.value)));
|
|
159
|
+
if (!hit) { problems.push(`${bench.name}: value ${bench.value} not in any row named after it`); continue; }
|
|
160
|
+
const cells = cellsOf(hit.r);
|
|
161
|
+
const header = headerNaming(rows, hit.index, modelNames);
|
|
162
|
+
if (!header) { warnings.push(`${bench.name}: no header naming the model above the row — check by hand`); continue; }
|
|
163
|
+
const valueAt = cells.map((c, i) => (sameNumber(c, bench.value) ? i : -1)).filter((i) => i >= 0);
|
|
164
|
+
const owns = valueAt.some((i) => header[i] && namesModel(header[i], modelNames));
|
|
165
|
+
if (!owns) problems.push(`${bench.name}: value sits under "${valueAt.map((i) => header[i] ?? '?').join(', ')}"`);
|
|
166
|
+
}
|
|
167
|
+
return { problems, warnings };
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
const raw = input === '-' ? fs.readFileSync(0, 'utf-8') : fs.readFileSync(input, 'utf-8');
|
|
171
|
+
let models;
|
|
172
|
+
try {
|
|
173
|
+
const parsed = JSON.parse(raw);
|
|
174
|
+
models = Array.isArray(parsed) ? parsed : Object.values(parsed);
|
|
175
|
+
} catch {
|
|
176
|
+
models = proposalObjects(raw);
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
let clean = 0, flagged = 0, warned = 0;
|
|
180
|
+
for (const model of models) {
|
|
181
|
+
const { problems, warnings } = await verify(model, model.id);
|
|
182
|
+
if (problems.length) {
|
|
183
|
+
flagged++;
|
|
184
|
+
console.log(`✗ ${model.id}\n ${problems.join('\n ')}`);
|
|
185
|
+
} else if (warnings.length) {
|
|
186
|
+
warned++;
|
|
187
|
+
console.log(`! ${model.id}\n ${warnings.join('\n ')}`);
|
|
188
|
+
} else {
|
|
189
|
+
clean++;
|
|
190
|
+
console.log(`✓ ${model.id}`);
|
|
191
|
+
}
|
|
192
|
+
}
|
|
193
|
+
console.log(`\n${models.length} proposal(s): ${clean} verified, ${warned} to check by hand, ${flagged} rejected`);
|
|
194
|
+
process.exit(flagged > 0 ? 1 : 0);
|