106 lines
3.4 KiB
JavaScript
106 lines
3.4 KiB
JavaScript
#!/usr/bin/env node
|
|
// Sprint 10 Task 1.2 — Sonnet route repair smoke test.
|
|
//
|
|
// One-shot HTTP probe that verifies the `claude-sonnet-4-6` LiteLLM
|
|
// route returns 200 (not 404 / model_not_found) on the repaired target.
|
|
// This is the minimal acceptance check per brief §1.2 — the full
|
|
// functional regression is Task 1.3 (judge calibration re-run).
|
|
//
|
|
// Exits:
|
|
// 0 — route returns HTTP 200 with a valid chat completion
|
|
// 1 — route returned a non-2xx status OR no completion content
|
|
// 2 — fetch failed (network / DNS / LiteLLM proxy down)
|
|
//
|
|
// Usage:
|
|
// node scripts/smoke-sonnet-route.mjs [--model claude-sonnet-4-6]
|
|
// [--litellm-url http://localhost:4000]
|
|
//
|
|
// Environment:
|
|
// LITELLM_BASE_URL, LITELLM_MASTER_KEY — same as all Waggle inference paths.
|
|
// ANTHROPIC_API_KEY must be provisioned in the LiteLLM container env for
|
|
// the probe to actually reach Anthropic.
|
|
|
|
const args = (() => {
|
|
const out = {
|
|
model: 'claude-sonnet-4-6',
|
|
litellmUrl: process.env.LITELLM_BASE_URL ?? 'http://localhost:4000',
|
|
litellmKey: process.env.LITELLM_MASTER_KEY ?? 'sk-waggle-dev',
|
|
};
|
|
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 === '--model') { out.model = next; i++; }
|
|
else if (flag === '--litellm-url') { out.litellmUrl = next; i++; }
|
|
else if (flag === '--litellm-key') { out.litellmKey = next; i++; }
|
|
}
|
|
return out;
|
|
})();
|
|
|
|
async function main() {
|
|
const url = `${args.litellmUrl.replace(/\/$/, '')}/v1/chat/completions`;
|
|
const started = Date.now();
|
|
let res;
|
|
try {
|
|
res = await fetch(url, {
|
|
method: 'POST',
|
|
headers: {
|
|
'Content-Type': 'application/json',
|
|
Authorization: `Bearer ${args.litellmKey}`,
|
|
},
|
|
body: JSON.stringify({
|
|
model: args.model,
|
|
messages: [
|
|
{ role: 'user', content: 'Respond with the single word OK.' },
|
|
],
|
|
max_tokens: 16,
|
|
temperature: 0.0,
|
|
}),
|
|
});
|
|
} catch (err) {
|
|
const msg = err instanceof Error ? err.message : String(err);
|
|
console.error(`[smoke-sonnet:FAIL] fetch error → ${msg}`);
|
|
process.exit(2);
|
|
}
|
|
const latencyMs = Date.now() - started;
|
|
const bodyText = await res.text();
|
|
let body;
|
|
try {
|
|
body = JSON.parse(bodyText);
|
|
} catch {
|
|
body = null;
|
|
}
|
|
|
|
if (!res.ok) {
|
|
const looksLikeModelNotFound =
|
|
res.status === 404
|
|
|| /model[_ -]?not[_ -]?found/i.test(bodyText)
|
|
|| /unknown model/i.test(bodyText);
|
|
const tag = looksLikeModelNotFound ? 'MODEL_NOT_FOUND' : 'HTTP_ERROR';
|
|
console.error(
|
|
`[smoke-sonnet:FAIL] ${tag} — http=${res.status} model=${args.model} `
|
|
+ `latency_ms=${latencyMs} body_head=${bodyText.slice(0, 240)}`,
|
|
);
|
|
process.exit(1);
|
|
}
|
|
|
|
const content = body?.choices?.[0]?.message?.content;
|
|
if (typeof content !== 'string' || content.length === 0) {
|
|
console.error(
|
|
`[smoke-sonnet:FAIL] empty_completion — http=${res.status} model=${args.model} `
|
|
+ `latency_ms=${latencyMs} body_head=${bodyText.slice(0, 240)}`,
|
|
);
|
|
process.exit(1);
|
|
}
|
|
|
|
const usage = body?.usage ?? {};
|
|
console.log(
|
|
`[smoke-sonnet:PASS] http=${res.status} model=${args.model} `
|
|
+ `latency_ms=${latencyMs} prompt_tokens=${usage.prompt_tokens ?? 0} `
|
|
+ `completion_tokens=${usage.completion_tokens ?? 0} content=${JSON.stringify(content.slice(0, 80))}`,
|
|
);
|
|
process.exit(0);
|
|
}
|
|
|
|
main();
|