moving
Some checks failed
Installer Smoke / installer-smoke (push) Has been cancelled

This commit is contained in:
Oleg Maslov
2026-09-02 10:10:29 +02:00
commit 0c3e2ead3b
3841 changed files with 970576 additions and 0 deletions

View File

@@ -0,0 +1,183 @@
#!/usr/bin/env node
// Sprint 10 Task 2.1 — Vendor availability probe.
//
// Three-vendor ensemble LOCKED per brief §2:
// 1. Anthropic Opus 4.7 (already production — Sprint 9 baseline)
// 2. OpenAI GPT-5.4 (config wired for v5 eval — provisioning TBD)
// 3. Google Gemini 3.1 Pro (config wired for v5 eval — provisioning TBD)
//
// This probe sends a minimal "respond with OK" prompt to each vendor
// through LiteLLM and reports per-vendor status. If any vendor is not
// provisionable (404 model_not_found, 401 auth, 402 billing, 429 rate
// limit, or hard timeout), it surfaces as a Day-1 blocker so Marko can
// resolve before Task 2.2 Fleiss' kappa baseline depends on the full
// ensemble path.
//
// Budget: ~3 × 20-token completions ≈ $0.001 total. Well under $5 Task
// 2.1 ceiling.
//
// Usage:
// node scripts/vendor-availability-probe.mjs
// [--litellm-url http://localhost:4000]
// [--out preflight-results/vendor-availability-<ISO>.json]
//
// Exits:
// 0 — all three vendors returned HTTP 200 with non-empty completions
// 1 — at least one vendor failed; details in stdout + JSON artifact
import fs from 'node:fs';
import path from 'node:path';
const VENDORS = [
{ id: 'anthropic', model: 'claude-opus-4-7', label: 'Anthropic Opus 4.7' },
{ id: 'openai', model: 'gpt-5.4', label: 'OpenAI GPT-5.4' },
{ id: 'google', model: 'gemini-3.1-pro', label: 'Google Gemini 3.1 Pro' },
];
const args = (() => {
const out = {
litellmUrl: process.env.LITELLM_BASE_URL ?? 'http://localhost:4000',
litellmKey: process.env.LITELLM_MASTER_KEY ?? 'sk-waggle-dev',
out: undefined,
};
const argv = process.argv.slice(2);
for (let i = 0; i < argv.length; i++) {
const flag = argv[i];
const next = argv[i + 1];
if (flag === '--litellm-url') { out.litellmUrl = next; i++; }
else if (flag === '--litellm-key') { out.litellmKey = next; i++; }
else if (flag === '--out') { out.out = next; i++; }
}
if (!out.out) {
const iso = new Date().toISOString().replace(/[:.]/g, '-');
out.out = `preflight-results/vendor-availability-${iso}.json`;
}
return out;
})();
async function probeVendor(vendor) {
const url = `${args.litellmUrl.replace(/\/$/, '')}/v1/chat/completions`;
const started = Date.now();
// Opus 4.7 / GPT-5 / o3 / o4 families reject `temperature` with HTTP
// 400. Match the heuristic the judge-client already uses so this
// probe doesn't flag them falsely.
const rejectsTemperature = /opus-4-7|gpt-5|o3|o4/i.test(vendor.model);
const reqBody = {
model: vendor.model,
messages: [{ role: 'user', content: 'Respond with the single word OK.' }],
max_tokens: 16,
};
if (!rejectsTemperature) reqBody.temperature = 0.0;
let res;
try {
res = await fetch(url, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
Authorization: `Bearer ${args.litellmKey}`,
},
body: JSON.stringify(reqBody),
});
} catch (err) {
const msg = err instanceof Error ? err.message : String(err);
return {
...vendor,
status: 'fetch_error',
http: null,
latencyMs: Date.now() - started,
error: msg,
completionText: null,
promptTokens: 0,
completionTokens: 0,
};
}
const latencyMs = Date.now() - started;
const bodyText = await res.text();
let body;
try { body = JSON.parse(bodyText); } catch { body = null; }
if (!res.ok) {
const lc = bodyText.toLowerCase();
let statusTag;
if (res.status === 401 || /unauthori[sz]ed/.test(lc)) statusTag = 'auth_error';
else if (res.status === 402 || /billing|payment|quota/.test(lc)) statusTag = 'billing_error';
else if (res.status === 404 || /model[_ -]?not[_ -]?found|unknown model/.test(lc)) statusTag = 'model_not_found';
else if (res.status === 429) statusTag = 'rate_limited';
else statusTag = 'http_error';
return {
...vendor,
status: statusTag,
http: res.status,
latencyMs,
error: bodyText.slice(0, 400),
completionText: null,
promptTokens: 0,
completionTokens: 0,
};
}
const content = body?.choices?.[0]?.message?.content;
if (typeof content !== 'string' || content.length === 0) {
return {
...vendor,
status: 'empty_completion',
http: res.status,
latencyMs,
error: `empty content — body_head=${bodyText.slice(0, 200)}`,
completionText: null,
promptTokens: body?.usage?.prompt_tokens ?? 0,
completionTokens: body?.usage?.completion_tokens ?? 0,
};
}
return {
...vendor,
status: 'ok',
http: res.status,
latencyMs,
error: null,
completionText: content.trim().slice(0, 120),
promptTokens: body?.usage?.prompt_tokens ?? 0,
completionTokens: body?.usage?.completion_tokens ?? 0,
};
}
async function main() {
console.log(`[vendor-probe] starting — litellm=${args.litellmUrl} vendors=${VENDORS.length}`);
const results = [];
for (const vendor of VENDORS) {
process.stdout.write(` ${vendor.id.padEnd(10, ' ')} ${vendor.model.padEnd(22, ' ')} ... `);
const r = await probeVendor(vendor);
results.push(r);
if (r.status === 'ok') {
console.log(`PASS (http=${r.http} latency=${r.latencyMs}ms completion=${JSON.stringify(r.completionText)})`);
} else {
console.log(`FAIL (${r.status}${r.http ? ` http=${r.http}` : ''} latency=${r.latencyMs}ms)`);
if (r.error) console.log(` error: ${r.error.slice(0, 280)}`);
}
}
const allOk = results.every(r => r.status === 'ok');
const payload = {
generatedAt: new Date().toISOString(),
litellmUrl: args.litellmUrl,
allAvailable: allOk,
vendors: results,
};
fs.mkdirSync(path.dirname(args.out), { recursive: true });
fs.writeFileSync(args.out, JSON.stringify(payload, null, 2) + '\n', 'utf-8');
console.log('');
console.log(
`[vendor-probe:summary] all_available=${allOk} ` +
`passes=${results.filter(r => r.status === 'ok').length}/${results.length} ` +
`out=${args.out}`,
);
process.exit(allOk ? 0 : 1);
}
main().catch(err => {
console.error('[vendor-probe:error]', err?.message ?? err);
process.exit(2);
});