moving
This commit is contained in:
319
benchmarks/gaia2/scripts/run-dry-verification.ts
Normal file
319
benchmarks/gaia2/scripts/run-dry-verification.ts
Normal file
@@ -0,0 +1,319 @@
|
||||
#!/usr/bin/env tsx
|
||||
/**
|
||||
* Sesija C Phase 3b — Gaia2 dry-verification driver (OpenRouter direct).
|
||||
*
|
||||
* Wires:
|
||||
* - benchmarks/gaia2/config.yaml → Gaia2AdapterConfig
|
||||
* - benchmarks/gaia2/data/*.jsonl → loaded via adapter.loadGaia2TasksFromJsonl
|
||||
* - OPENROUTER_API_KEY → LlmCallFn (OpenRouter direct chat-completions)
|
||||
* - benchmarks/gaia2/adapter.ts → runDryRunSweep
|
||||
* - benchmarks/gaia2/runs/<ISO>/ → JSONL output
|
||||
*
|
||||
* Phase 3b-B-1 patch: replaced LiteLLM proxy transport with OpenRouter
|
||||
* direct per PM ratification Option B (2026-04-30 Phase 3b-B kick-off
|
||||
* env-var halt resolved). LITELLM_URL + LITELLM_MASTER_KEY were empty
|
||||
* in the shell; OPENROUTER_API_KEY was present. Proportional pivot for
|
||||
* 4-invocation probe scope.
|
||||
*
|
||||
* Slug map (`LITELLM_TO_OPENROUTER_SLUG`):
|
||||
* - claude-opus-4-7 → anthropic/claude-opus-4.7 (EXACT match)
|
||||
* - qwen3.6-35b-a3b → qwen/qwen3-30b-a3b-thinking-2507
|
||||
* (closest available; 30b vs 35b size delta;
|
||||
* same a3b architecture + thinking-mode;
|
||||
* proportional for cost-projection probe)
|
||||
*
|
||||
* Cost methodology: per-call cost = inTokens × priceIn/1e6 +
|
||||
* outTokens × priceOut/1e6, prices from Faza 1 PRICE_TABLE
|
||||
* (decisions/2026-04-29-gepa-faza1-results.md §F). Opus prices are
|
||||
* exact (Faza 1 used same model). Qwen prices are the Faza 1
|
||||
* `qwen3.6-35b-a3b-via-openrouter` row applied to the 30b OpenRouter
|
||||
* slug as projection-grade approximation.
|
||||
*
|
||||
* Usage:
|
||||
* pnpm tsx benchmarks/gaia2/scripts/run-dry-verification.ts \\
|
||||
* --tasks benchmarks/gaia2/data/tasks-mini-2.jsonl \\
|
||||
* --halt-after-probe # (Phase 3b-B-2 sample mode)
|
||||
*
|
||||
* Env required:
|
||||
* OPENROUTER_API_KEY (required; FATAL if unset)
|
||||
*/
|
||||
|
||||
import * as fs from 'node:fs';
|
||||
import * as path from 'node:path';
|
||||
import { pathToFileURL } from 'node:url';
|
||||
import * as yaml from 'js-yaml';
|
||||
import {
|
||||
runDryRunSweep,
|
||||
writeRecordsToJsonl,
|
||||
type Gaia2AdapterConfig,
|
||||
type LlmCallFn,
|
||||
} from '../adapter.js';
|
||||
|
||||
// ─── OpenRouter constants ────────────────────────────────────────────
|
||||
|
||||
const OPENROUTER_URL = 'https://openrouter.ai/api/v1/chat/completions';
|
||||
|
||||
const SUBJECT_MAX_TOKENS_QWEN = 16_000;
|
||||
const SUBJECT_MAX_TOKENS_DEFAULT = 4_096;
|
||||
|
||||
/**
|
||||
* Translate adapter.ts SHAPE_TO_MODEL names (LiteLLM aliases) into
|
||||
* OpenRouter slugs. Validated 2026-04-30 against
|
||||
* https://openrouter.ai/api/v1/models response.
|
||||
*
|
||||
* If a shape's underlying model is missing from this map at runtime,
|
||||
* the call fails fast with a clear error.
|
||||
*/
|
||||
const LITELLM_TO_OPENROUTER_SLUG: Readonly<Record<string, string>> = Object.freeze({
|
||||
'claude-opus-4-7': 'anthropic/claude-opus-4.7',
|
||||
'qwen3.6-35b-a3b': 'qwen/qwen3-30b-a3b-thinking-2507',
|
||||
// Fallbacks for shapes not in the brief 4-shape sweep but reachable
|
||||
// via SHAPE_TO_MODEL in adapter.ts (defensive — would only fire if
|
||||
// config.yaml is widened beyond BRIEF_DRY_RUN_SHAPES).
|
||||
'gpt-5.4': 'openai/gpt-4o-2024-08-06', // closest available; flag if used
|
||||
'claude-haiku-4-5': 'anthropic/claude-haiku-4.5',
|
||||
});
|
||||
|
||||
/**
|
||||
* Faza 1 PRICE_TABLE rows (USD per 1M tokens), used for projection-grade
|
||||
* cost computation when OpenRouter response doesn't include `usage.cost`
|
||||
* (which requires `usage.include: true` in request body).
|
||||
*
|
||||
* Source: benchmarks/gepa/scripts/faza-1/run-checkpoint-c.ts:106-110.
|
||||
* Opus prices are wire-accurate. Qwen 30b prices are approximation
|
||||
* inherited from `qwen3.6-35b-a3b-via-openrouter`.
|
||||
*/
|
||||
const PRICE_TABLE: Readonly<Record<string, { in: number; out: number }>> = Object.freeze({
|
||||
'anthropic/claude-opus-4.7': { in: 15.0, out: 75.0 },
|
||||
'qwen/qwen3-30b-a3b-thinking-2507': { in: 0.6, out: 2.4 },
|
||||
'openai/gpt-4o-2024-08-06': { in: 2.5, out: 10.0 },
|
||||
'anthropic/claude-haiku-4.5': { in: 1.0, out: 5.0 },
|
||||
});
|
||||
|
||||
// ─── OpenRouter LlmCallFn ────────────────────────────────────────────
|
||||
|
||||
function makeOpenRouterCallFn(opts: { apiKey: string }): LlmCallFn {
|
||||
return async (input) => {
|
||||
const slug = LITELLM_TO_OPENROUTER_SLUG[input.model];
|
||||
if (!slug) {
|
||||
return {
|
||||
content: '',
|
||||
inTokens: 0,
|
||||
outTokens: 0,
|
||||
costUsd: 0,
|
||||
latencyMs: 0,
|
||||
error: `No OpenRouter slug mapping for model "${input.model}". Add to LITELLM_TO_OPENROUTER_SLUG in run-dry-verification.ts.`,
|
||||
};
|
||||
}
|
||||
|
||||
const isQwen = slug.includes('qwen');
|
||||
const maxTokens = input.maxTokens ?? (isQwen ? SUBJECT_MAX_TOKENS_QWEN : SUBJECT_MAX_TOKENS_DEFAULT);
|
||||
|
||||
const payload: Record<string, unknown> = {
|
||||
model: slug,
|
||||
messages: input.messages,
|
||||
max_tokens: maxTokens,
|
||||
// OpenRouter `usage.include: true` opt-in returns wire-accurate cost
|
||||
// in the response. We capture this when present and fall back to
|
||||
// PRICE_TABLE-based computation otherwise.
|
||||
usage: { include: true },
|
||||
};
|
||||
|
||||
// Temperature: Opus needs 1.0 sentinel per Faza 1 convention; GPT-5.x
|
||||
// family rejects temperature override; everything else gets 0.3.
|
||||
if (slug.startsWith('anthropic/claude-opus')) {
|
||||
payload.temperature = 1.0;
|
||||
} else if (slug.startsWith('openai/gpt-5')) {
|
||||
// omit temperature (provider rejects override)
|
||||
} else {
|
||||
payload.temperature = input.temperature ?? 0.3;
|
||||
}
|
||||
|
||||
// Qwen thinking-mode is encoded in the slug itself (`-thinking-2507`)
|
||||
// for the OpenRouter route — no `enable_thinking` extra_body needed.
|
||||
// Verified against OpenRouter qwen3-*-thinking-2507 model cards.
|
||||
|
||||
const startMs = Date.now();
|
||||
let lastErr: string | undefined;
|
||||
|
||||
for (let attempt = 0; attempt < 2; attempt++) {
|
||||
try {
|
||||
const resp = await fetch(OPENROUTER_URL, {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
Authorization: `Bearer ${opts.apiKey}`,
|
||||
// OpenRouter conventions:
|
||||
'HTTP-Referer': 'https://waggle-os.ai',
|
||||
// ASCII-only — fetch() rejects non-ByteString header values
|
||||
// (U+2014 em dash etc. throw "Cannot convert argument to a
|
||||
// ByteString" at request time).
|
||||
'X-Title': 'Waggle Sesija C - Gaia2 narrow-proxy probe',
|
||||
},
|
||||
body: JSON.stringify(payload),
|
||||
});
|
||||
|
||||
const body = (await resp.json()) as Record<string, unknown>;
|
||||
|
||||
if ('error' in body) {
|
||||
const errObj = body.error as Record<string, unknown> | undefined;
|
||||
lastErr = String(errObj?.message ?? JSON.stringify(body.error)).slice(0, 300);
|
||||
if (attempt < 1) {
|
||||
await new Promise((r) => setTimeout(r, 1500));
|
||||
continue;
|
||||
}
|
||||
return {
|
||||
content: '',
|
||||
inTokens: 0,
|
||||
outTokens: 0,
|
||||
costUsd: 0,
|
||||
latencyMs: Date.now() - startMs,
|
||||
error: lastErr,
|
||||
};
|
||||
}
|
||||
|
||||
const choices = body.choices as
|
||||
| Array<{ message?: { content?: string; reasoning?: string } }>
|
||||
| undefined;
|
||||
const content = choices?.[0]?.message?.content ?? '';
|
||||
|
||||
const usage = body.usage as
|
||||
| { prompt_tokens?: number; completion_tokens?: number; cost?: number }
|
||||
| undefined;
|
||||
const inTokens = usage?.prompt_tokens ?? 0;
|
||||
const outTokens = usage?.completion_tokens ?? 0;
|
||||
|
||||
// Wire-accurate cost preferred; fall back to PRICE_TABLE.
|
||||
let costUsd = usage?.cost ?? 0;
|
||||
if (costUsd === 0 && PRICE_TABLE[slug]) {
|
||||
const pt = PRICE_TABLE[slug];
|
||||
costUsd = (inTokens * pt.in) / 1_000_000 + (outTokens * pt.out) / 1_000_000;
|
||||
}
|
||||
|
||||
return {
|
||||
content,
|
||||
inTokens,
|
||||
outTokens,
|
||||
costUsd,
|
||||
latencyMs: Date.now() - startMs,
|
||||
};
|
||||
} catch (err: unknown) {
|
||||
lastErr = err instanceof Error ? err.message : String(err);
|
||||
if (attempt < 1) {
|
||||
await new Promise((r) => setTimeout(r, 1500));
|
||||
continue;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
content: '',
|
||||
inTokens: 0,
|
||||
outTokens: 0,
|
||||
costUsd: 0,
|
||||
latencyMs: Date.now() - startMs,
|
||||
error: lastErr ?? 'unknown OpenRouter call failure',
|
||||
};
|
||||
};
|
||||
}
|
||||
|
||||
// ─── CLI ──────────────────────────────────────────────────────────────
|
||||
|
||||
function parseArgs(): { tasksFile: string; haltAfterProbe: boolean; configPath: string } {
|
||||
const args = process.argv.slice(2);
|
||||
let tasksFile = 'benchmarks/gaia2/data/tasks-mini-10.jsonl';
|
||||
let haltAfterProbe = false;
|
||||
let configPath = 'benchmarks/gaia2/config.yaml';
|
||||
for (let i = 0; i < args.length; i++) {
|
||||
const a = args[i];
|
||||
if (a === '--tasks' && i + 1 < args.length) {
|
||||
tasksFile = args[i + 1];
|
||||
i++;
|
||||
} else if (a === '--halt-after-probe') {
|
||||
haltAfterProbe = true;
|
||||
} else if (a === '--config' && i + 1 < args.length) {
|
||||
configPath = args[i + 1];
|
||||
i++;
|
||||
}
|
||||
}
|
||||
return { tasksFile, haltAfterProbe, configPath };
|
||||
}
|
||||
|
||||
function loadConfig(configPath: string): Gaia2AdapterConfig {
|
||||
const raw = fs.readFileSync(configPath, 'utf-8');
|
||||
const parsed = yaml.load(raw) as Record<string, unknown>;
|
||||
return parsed as unknown as Gaia2AdapterConfig;
|
||||
}
|
||||
|
||||
async function main(): Promise<number> {
|
||||
const { tasksFile, haltAfterProbe, configPath } = parseArgs();
|
||||
|
||||
console.error(
|
||||
`[gaia2-dry-verification] config=${configPath} tasks=${tasksFile} ` +
|
||||
`haltAfterProbe=${haltAfterProbe}`,
|
||||
);
|
||||
|
||||
const config = loadConfig(configPath);
|
||||
|
||||
const apiKey = process.env.OPENROUTER_API_KEY;
|
||||
if (!apiKey || apiKey.length === 0) {
|
||||
console.error(
|
||||
'FATAL: OPENROUTER_API_KEY must be set in env (Phase 3b-B-1 OpenRouter direct fallback per PM ratification Option B).',
|
||||
);
|
||||
return 2;
|
||||
}
|
||||
const llmCall = makeOpenRouterCallFn({ apiKey });
|
||||
|
||||
const startedAt = new Date().toISOString().replace(/[:.]/g, '-');
|
||||
const outDir = path.resolve(
|
||||
process.cwd(),
|
||||
config.output_dir_root,
|
||||
`dry-verification-${startedAt}`,
|
||||
);
|
||||
fs.mkdirSync(outDir, { recursive: true });
|
||||
|
||||
const result = await runDryRunSweep(config, llmCall, {
|
||||
tasksFile,
|
||||
haltAfterProbe,
|
||||
});
|
||||
|
||||
writeRecordsToJsonl(result.probe_records, path.join(outDir, 'probe.jsonl'));
|
||||
writeRecordsToJsonl(result.full_sweep_records, path.join(outDir, 'full-sweep.jsonl'));
|
||||
|
||||
const summary = {
|
||||
timestamp_started: new Date().toISOString(),
|
||||
config_path: configPath,
|
||||
tasks_file: tasksFile,
|
||||
transport: 'openrouter-direct' as const,
|
||||
probe_invocation_count: result.probe_records.length,
|
||||
probe_cost_usd: result.probe_cost_usd,
|
||||
projected_total_usd: result.projected_total_usd,
|
||||
halt_triggered: result.halt_triggered,
|
||||
full_sweep_invocation_count: result.full_sweep_records.length,
|
||||
final_total_cost_usd: result.final_total_cost_usd,
|
||||
cost_cap_usd: config.cost_cap_usd,
|
||||
halt_trigger_usd: config.halt_trigger_usd,
|
||||
slug_map: LITELLM_TO_OPENROUTER_SLUG,
|
||||
price_table: PRICE_TABLE,
|
||||
};
|
||||
fs.writeFileSync(
|
||||
path.join(outDir, 'summary.json'),
|
||||
JSON.stringify(summary, null, 2),
|
||||
'utf-8',
|
||||
);
|
||||
|
||||
console.error(`[gaia2-dry-verification] complete — output dir: ${outDir}`);
|
||||
console.error(JSON.stringify(summary, null, 2));
|
||||
|
||||
return result.halt_triggered ? 1 : 0;
|
||||
}
|
||||
|
||||
if (import.meta.url === pathToFileURL(process.argv[1] ?? '').href) {
|
||||
main().then(
|
||||
(code) => process.exit(code),
|
||||
(err) => {
|
||||
console.error('FATAL:', err instanceof Error ? err.stack : err);
|
||||
process.exit(3);
|
||||
},
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user