instar 1.3.780 → 1.3.781
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/dashboard/index.html +161 -0
- package/dist/core/PostUpdateMigrator.d.ts +21 -0
- package/dist/core/PostUpdateMigrator.d.ts.map +1 -1
- package/dist/core/PostUpdateMigrator.js +59 -0
- package/dist/core/PostUpdateMigrator.js.map +1 -1
- package/dist/core/routingPriceAuthority.d.ts +0 -0
- package/dist/core/routingPriceAuthority.d.ts.map +1 -0
- package/dist/core/routingPriceAuthority.js +0 -0
- package/dist/core/routingPriceAuthority.js.map +1 -0
- package/dist/core/routingSpendView.d.ts +172 -0
- package/dist/core/routingSpendView.d.ts.map +1 -0
- package/dist/core/routingSpendView.js +215 -0
- package/dist/core/routingSpendView.js.map +1 -0
- package/dist/core/types.d.ts +25 -0
- package/dist/core/types.d.ts.map +1 -1
- package/dist/core/types.js.map +1 -1
- package/dist/monitoring/FeatureMetricsLedger.d.ts +74 -2
- package/dist/monitoring/FeatureMetricsLedger.d.ts.map +1 -1
- package/dist/monitoring/FeatureMetricsLedger.js +233 -8
- package/dist/monitoring/FeatureMetricsLedger.js.map +1 -1
- package/dist/scaffold/templates.d.ts.map +1 -1
- package/dist/scaffold/templates.js +2 -1
- package/dist/scaffold/templates.js.map +1 -1
- package/dist/server/AgentServer.d.ts +1 -0
- package/dist/server/AgentServer.d.ts.map +1 -1
- package/dist/server/AgentServer.js +35 -2
- package/dist/server/AgentServer.js.map +1 -1
- package/dist/server/CapabilityIndex.d.ts.map +1 -1
- package/dist/server/CapabilityIndex.js +12 -0
- package/dist/server/CapabilityIndex.js.map +1 -1
- package/dist/server/routes.d.ts +3 -0
- package/dist/server/routes.d.ts.map +1 -1
- package/dist/server/routes.js +46 -0
- package/dist/server/routes.js.map +1 -1
- package/package.json +1 -1
- package/scripts/routing-price-refresh.mjs +216 -0
- package/scripts/routing-prices.manifest.json +64 -0
- package/src/data/builtin-manifest.json +64 -64
- package/src/scaffold/templates/jobs/instar/routing-price-refresh.md +30 -0
- package/src/scaffold/templates.ts +2 -1
- package/upgrades/1.3.781.md +25 -0
- package/upgrades/side-effects/routing-spend-increment-a.md +84 -0
package/package.json
CHANGED
|
@@ -0,0 +1,216 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/**
|
|
3
|
+
* routing-price-refresh.mjs — the deterministic prober behind the OFF-by-default
|
|
4
|
+
* `routing-price-refresh` job (docs/specs/routing-control-room-spend-alerts.md, FD-8).
|
|
5
|
+
*
|
|
6
|
+
* It re-confirms published per-token prices for the metered doors and writes them into
|
|
7
|
+
* the MACHINE-LOCAL OBSERVED CACHE ONLY (`.instar/routing-prices.observed.json`) —
|
|
8
|
+
* STRUCTURALLY never the canonical manifest (a lint + unit test assert this). Observed
|
|
9
|
+
* points feed the REPORTING view + the promote-me drift hint; they are gate-INELIGIBLE
|
|
10
|
+
* by construction (in Increment B the money gate reads only the canonical manifest).
|
|
11
|
+
*
|
|
12
|
+
* FD-8 discipline (all enforced here):
|
|
13
|
+
* - FORWARD-ONLY: every written point has `effectiveAt = today (UTC, day-aligned)`,
|
|
14
|
+
* `corrects: null`. It can never write a backdated point or a correction.
|
|
15
|
+
* - FREE-PROBE FIRST: `--scope free-probes` (default) queries only public, no-auth
|
|
16
|
+
* model-list endpoints (OpenRouter). Metered / web-verify probes are MANUAL-ONLY and
|
|
17
|
+
* refused unless a positive `--budget-usd` is passed (default 0 → refuse) — an
|
|
18
|
+
* unknown price refuses rather than guesses.
|
|
19
|
+
* - SANE-PRICE VALIDATION: a candidate failing the range / cached≤input checks is
|
|
20
|
+
* dropped (never written), matching the reporting authority's fail-closed load.
|
|
21
|
+
*
|
|
22
|
+
* Pure core (parse / forward-only-merge / validate) is unit-tested with fixtures — no
|
|
23
|
+
* network in tests. Usage:
|
|
24
|
+
* node scripts/routing-price-refresh.mjs [--scope free-probes|+liveness|+web-verify]
|
|
25
|
+
* [--budget-usd N] [--dry-run] [--out <path>] [--project-dir <dir>] [--state-dir <dir>]
|
|
26
|
+
*/
|
|
27
|
+
|
|
28
|
+
import * as fs from 'node:fs';
|
|
29
|
+
import * as path from 'node:path';
|
|
30
|
+
|
|
31
|
+
/** The metered (door → canonical model ids) we track prices for — kept in sync with the manifest. */
|
|
32
|
+
export const TRACKED = {
|
|
33
|
+
'openrouter-api': ['openai/gpt-5.5', 'anthropic/claude-opus-4-8'],
|
|
34
|
+
'gemini-api': ['gemini-3.1-flash-lite'],
|
|
35
|
+
'groq-api': ['openai/gpt-oss-120b'],
|
|
36
|
+
};
|
|
37
|
+
|
|
38
|
+
/** UTC-day-aligned ISO (T00:00:00Z) for a timestamp — every observed point is day-aligned (FD-18). */
|
|
39
|
+
export function dayAlignedIso(ms) {
|
|
40
|
+
const d = new Date(ms);
|
|
41
|
+
return new Date(Date.UTC(d.getUTCFullYear(), d.getUTCMonth(), d.getUTCDate())).toISOString();
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
function canonical(id) {
|
|
45
|
+
return String(id ?? '').trim().toLowerCase();
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
/** Sane-price validation (mirrors routingPriceAuthority.isValidPricePoint's money-safety checks). */
|
|
49
|
+
export function isSanePoint(p) {
|
|
50
|
+
if (!p || typeof p !== 'object') return false;
|
|
51
|
+
if (typeof p.door !== 'string' || !p.door.trim()) return false;
|
|
52
|
+
if (typeof p.modelId !== 'string' || !p.modelId.trim()) return false;
|
|
53
|
+
if (typeof p.inPerMtok !== 'number' || !Number.isFinite(p.inPerMtok) || p.inPerMtok < 0) return false;
|
|
54
|
+
if (typeof p.outPerMtok !== 'number' || !Number.isFinite(p.outPerMtok) || p.outPerMtok < 0) return false;
|
|
55
|
+
if (p.cachedInPerMtok !== undefined) {
|
|
56
|
+
if (typeof p.cachedInPerMtok !== 'number' || !Number.isFinite(p.cachedInPerMtok) || p.cachedInPerMtok < 0 || p.cachedInPerMtok > p.inPerMtok) return false;
|
|
57
|
+
}
|
|
58
|
+
if (typeof p.effectiveAt !== 'string' || dayAlignedIso(Date.parse(p.effectiveAt)) !== p.effectiveAt) return false;
|
|
59
|
+
return true;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
/**
|
|
63
|
+
* Parse OpenRouter's public /models payload into candidate observed points for the
|
|
64
|
+
* TRACKED openrouter models. OpenRouter reports `pricing.prompt`/`.completion` as USD
|
|
65
|
+
* PER TOKEN (string); we convert to USD per MILLION tokens. Pure.
|
|
66
|
+
*/
|
|
67
|
+
export function parseOpenRouterModels(payload, nowMs) {
|
|
68
|
+
const effectiveAt = dayAlignedIso(nowMs);
|
|
69
|
+
const out = [];
|
|
70
|
+
const data = payload && Array.isArray(payload.data) ? payload.data : [];
|
|
71
|
+
const tracked = new Set(TRACKED['openrouter-api'].map(canonical));
|
|
72
|
+
for (const m of data) {
|
|
73
|
+
if (!m || typeof m !== 'object') continue;
|
|
74
|
+
const modelId = canonical(m.id);
|
|
75
|
+
if (!tracked.has(modelId)) continue;
|
|
76
|
+
const pr = m.pricing;
|
|
77
|
+
if (!pr) continue;
|
|
78
|
+
const inPerMtok = Number(pr.prompt) * 1e6;
|
|
79
|
+
const outPerMtok = Number(pr.completion) * 1e6;
|
|
80
|
+
const point = {
|
|
81
|
+
door: 'openrouter-api',
|
|
82
|
+
modelId,
|
|
83
|
+
inPerMtok: Number.isFinite(inPerMtok) ? round6(inPerMtok) : NaN,
|
|
84
|
+
outPerMtok: Number.isFinite(outPerMtok) ? round6(outPerMtok) : NaN,
|
|
85
|
+
effectiveAt,
|
|
86
|
+
recordedAt: new Date(nowMs).toISOString(),
|
|
87
|
+
source: 'openrouter-models-api',
|
|
88
|
+
corrects: null,
|
|
89
|
+
};
|
|
90
|
+
if (isSanePoint(point)) out.push(point);
|
|
91
|
+
}
|
|
92
|
+
return out;
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
/**
|
|
96
|
+
* FORWARD-ONLY merge into the observed cache: keep existing observed points, and add a
|
|
97
|
+
* new candidate ONLY when its effectiveAt is ≥ every existing point for the same
|
|
98
|
+
* (door, model) — never rewrite history, never a same-day duplicate. Pure.
|
|
99
|
+
*/
|
|
100
|
+
export function mergeForwardOnly(existingPoints, candidates) {
|
|
101
|
+
const points = Array.isArray(existingPoints) ? existingPoints.slice() : [];
|
|
102
|
+
const latestByKey = new Map();
|
|
103
|
+
for (const p of points) {
|
|
104
|
+
const key = `${p.door} ${canonical(p.modelId)}`;
|
|
105
|
+
const eff = Date.parse(p.effectiveAt);
|
|
106
|
+
if (!latestByKey.has(key) || eff > latestByKey.get(key)) latestByKey.set(key, eff);
|
|
107
|
+
}
|
|
108
|
+
const added = [];
|
|
109
|
+
for (const c of candidates) {
|
|
110
|
+
if (!isSanePoint(c)) continue;
|
|
111
|
+
const key = `${c.door} ${canonical(c.modelId)}`;
|
|
112
|
+
const eff = Date.parse(c.effectiveAt);
|
|
113
|
+
const latest = latestByKey.get(key);
|
|
114
|
+
if (latest !== undefined && eff <= latest) continue; // forward-only: no backdate, no same-day dup
|
|
115
|
+
points.push(c);
|
|
116
|
+
latestByKey.set(key, eff);
|
|
117
|
+
added.push(c);
|
|
118
|
+
}
|
|
119
|
+
return { points, added };
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
function round6(n) {
|
|
123
|
+
return Math.round(n * 1e6) / 1e6;
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
function readObserved(outPath) {
|
|
127
|
+
try {
|
|
128
|
+
const raw = JSON.parse(fs.readFileSync(outPath, 'utf-8'));
|
|
129
|
+
return Array.isArray(raw?.points) ? raw.points : [];
|
|
130
|
+
} catch {
|
|
131
|
+
return [];
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
function writeObserved(outPath, points) {
|
|
136
|
+
const body = {
|
|
137
|
+
schemaVersion: 1,
|
|
138
|
+
_doc: 'MACHINE-LOCAL observed price cache written ONLY by scripts/routing-price-refresh.mjs (FD-8). REPORTING-ONLY — never gate-eligible; promote to the canonical manifest via the reviewed git/PIN path. Append/forward-only.',
|
|
139
|
+
points,
|
|
140
|
+
};
|
|
141
|
+
fs.mkdirSync(path.dirname(outPath), { recursive: true });
|
|
142
|
+
fs.writeFileSync(outPath, `${JSON.stringify(body, null, 2)}\n`);
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
async function fetchOpenRouterModels(timeoutMs = 8000) {
|
|
146
|
+
const ctrl = new AbortController();
|
|
147
|
+
const t = setTimeout(() => ctrl.abort(), timeoutMs);
|
|
148
|
+
try {
|
|
149
|
+
const resp = await fetch('https://openrouter.ai/api/v1/models', { signal: ctrl.signal });
|
|
150
|
+
if (!resp.ok) throw new Error(`openrouter models HTTP ${resp.status}`);
|
|
151
|
+
return await resp.json();
|
|
152
|
+
} finally {
|
|
153
|
+
clearTimeout(t);
|
|
154
|
+
}
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
function parseArgs(argv) {
|
|
158
|
+
const args = { scope: 'free-probes', budgetUsd: 0, dryRun: false, out: null, projectDir: process.cwd(), stateDir: null };
|
|
159
|
+
for (let i = 0; i < argv.length; i++) {
|
|
160
|
+
const a = argv[i];
|
|
161
|
+
if (a === '--scope') args.scope = argv[++i];
|
|
162
|
+
else if (a === '--budget-usd') args.budgetUsd = Number(argv[++i]);
|
|
163
|
+
else if (a === '--dry-run') args.dryRun = true;
|
|
164
|
+
else if (a === '--out') args.out = argv[++i];
|
|
165
|
+
else if (a === '--project-dir') args.projectDir = argv[++i];
|
|
166
|
+
else if (a === '--state-dir') args.stateDir = argv[++i];
|
|
167
|
+
}
|
|
168
|
+
return args;
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
async function main() {
|
|
172
|
+
const args = parseArgs(process.argv.slice(2));
|
|
173
|
+
const stateDir = args.stateDir ?? path.join(args.projectDir, '.instar');
|
|
174
|
+
const outPath = args.out ?? path.join(stateDir, 'routing-prices.observed.json');
|
|
175
|
+
const now = Date.now();
|
|
176
|
+
|
|
177
|
+
if (args.scope !== 'free-probes' && !(args.budgetUsd > 0)) {
|
|
178
|
+
// Metered / web-verify probes are MANUAL-ONLY + budget-capped (FD-8). No budget → refuse.
|
|
179
|
+
console.error(`[routing-price-refresh] scope '${args.scope}' requires a positive --budget-usd (metered probes are manual-only, budget-fail-closed). Refusing.`);
|
|
180
|
+
process.exit(2);
|
|
181
|
+
return;
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
const candidates = [];
|
|
185
|
+
const notes = [];
|
|
186
|
+
// Free scope: only publicly queryable doors. OpenRouter's /models is public + no-auth.
|
|
187
|
+
try {
|
|
188
|
+
const payload = await fetchOpenRouterModels();
|
|
189
|
+
const pts = parseOpenRouterModels(payload, now);
|
|
190
|
+
candidates.push(...pts);
|
|
191
|
+
notes.push(`openrouter-api: ${pts.length} tracked model price(s) probed`);
|
|
192
|
+
} catch (err) {
|
|
193
|
+
notes.push(`openrouter-api: probe failed (${err?.message ?? err}) — skipped, no data written for this door`);
|
|
194
|
+
}
|
|
195
|
+
// gemini-api / groq-api need a key → out of the free scope. Honestly reported, never guessed.
|
|
196
|
+
notes.push('gemini-api, groq-api: need an API key → not in free-probe scope (manual metered probe only)');
|
|
197
|
+
|
|
198
|
+
const existing = readObserved(outPath);
|
|
199
|
+
const { points, added } = mergeForwardOnly(existing, candidates);
|
|
200
|
+
|
|
201
|
+
if (args.dryRun) {
|
|
202
|
+
console.log(JSON.stringify({ dryRun: true, wouldAdd: added, notes }, null, 2));
|
|
203
|
+
return;
|
|
204
|
+
}
|
|
205
|
+
if (added.length > 0) writeObserved(outPath, points);
|
|
206
|
+
console.log(JSON.stringify({ added: added.length, totalObserved: points.length, out: outPath, notes }, null, 2));
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
// Only run when invoked directly (import for unit tests stays side-effect-free).
|
|
210
|
+
const invokedDirectly = process.argv[1] && path.resolve(process.argv[1]) === path.resolve(new URL(import.meta.url).pathname);
|
|
211
|
+
if (invokedDirectly) {
|
|
212
|
+
main().catch((err) => {
|
|
213
|
+
console.error('[routing-price-refresh] fatal:', err?.message ?? err);
|
|
214
|
+
process.exit(1);
|
|
215
|
+
});
|
|
216
|
+
}
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
{
|
|
2
|
+
"schemaVersion": 1,
|
|
3
|
+
"version": 1,
|
|
4
|
+
"_doc": "Canonical, human-reviewed price authority for the Routing Control Room spend view (docs/specs/routing-control-room-spend-alerts.md, Layer 1). USD per MILLION tokens, per door+model, effective-dated. APPEND-ONLY: a change or correction ADDS a point; points are never edited in place. GENERIC PUBLISHED prices only — operator-specific deals live in the machine-local overlay (.instar/routing-prices.overlay.json), never here. Written ONLY by a human path (a reviewed git commit, or — in Increment B — the PIN-gated promotion action). Every point's effectiveAt MUST be UTC-day-aligned (T00:00:00Z). In Increment A this manifest feeds the REPORTING view only; no money gate consumes it yet (the gate is Increment B).",
|
|
5
|
+
"doors": {
|
|
6
|
+
"gemini-api": {
|
|
7
|
+
"freshnessSlaDays": 45,
|
|
8
|
+
"staleMode": "book-conservative-max",
|
|
9
|
+
"conservativeMax": { "inPerMtok": 2.0, "outPerMtok": 12.0 }
|
|
10
|
+
},
|
|
11
|
+
"openrouter-api": {
|
|
12
|
+
"freshnessSlaDays": 45,
|
|
13
|
+
"staleMode": "book-conservative-max",
|
|
14
|
+
"conservativeMax": { "inPerMtok": 15.0, "outPerMtok": 90.0 }
|
|
15
|
+
},
|
|
16
|
+
"groq-api": {
|
|
17
|
+
"freshnessSlaDays": 45,
|
|
18
|
+
"staleMode": "book-conservative-max",
|
|
19
|
+
"conservativeMax": { "inPerMtok": 2.0, "outPerMtok": 8.0 }
|
|
20
|
+
}
|
|
21
|
+
},
|
|
22
|
+
"points": [
|
|
23
|
+
{
|
|
24
|
+
"door": "gemini-api",
|
|
25
|
+
"modelId": "gemini-3.1-flash-lite",
|
|
26
|
+
"inPerMtok": 0.25,
|
|
27
|
+
"outPerMtok": 1.5,
|
|
28
|
+
"effectiveAt": "2026-07-01T00:00:00Z",
|
|
29
|
+
"recordedAt": "2026-07-05T00:00:00Z",
|
|
30
|
+
"source": "published-generic-list",
|
|
31
|
+
"corrects": null
|
|
32
|
+
},
|
|
33
|
+
{
|
|
34
|
+
"door": "openrouter-api",
|
|
35
|
+
"modelId": "openai/gpt-5.5",
|
|
36
|
+
"inPerMtok": 5.0,
|
|
37
|
+
"outPerMtok": 30.0,
|
|
38
|
+
"effectiveAt": "2026-07-01T00:00:00Z",
|
|
39
|
+
"recordedAt": "2026-07-05T00:00:00Z",
|
|
40
|
+
"source": "published-generic-list",
|
|
41
|
+
"corrects": null
|
|
42
|
+
},
|
|
43
|
+
{
|
|
44
|
+
"door": "openrouter-api",
|
|
45
|
+
"modelId": "anthropic/claude-opus-4-8",
|
|
46
|
+
"inPerMtok": 5.0,
|
|
47
|
+
"outPerMtok": 25.0,
|
|
48
|
+
"effectiveAt": "2026-07-01T00:00:00Z",
|
|
49
|
+
"recordedAt": "2026-07-05T00:00:00Z",
|
|
50
|
+
"source": "published-generic-list",
|
|
51
|
+
"corrects": null
|
|
52
|
+
},
|
|
53
|
+
{
|
|
54
|
+
"door": "groq-api",
|
|
55
|
+
"modelId": "openai/gpt-oss-120b",
|
|
56
|
+
"inPerMtok": 0.2,
|
|
57
|
+
"outPerMtok": 0.8,
|
|
58
|
+
"effectiveAt": "2026-07-01T00:00:00Z",
|
|
59
|
+
"recordedAt": "2026-07-05T00:00:00Z",
|
|
60
|
+
"source": "published-generic-list",
|
|
61
|
+
"corrects": null
|
|
62
|
+
}
|
|
63
|
+
]
|
|
64
|
+
}
|