mirror of
https://github.com/pewdiepie-archdaemon/odysseus.git
synced 2026-09-15 20:52:21 +02:00
132 lines
7.9 KiB
JavaScript
132 lines
7.9 KiB
JavaScript
#!/usr/bin/env node
|
|
/** Real 7011 image attachment -> answer -> reload -> image follow-up check. */
|
|
import fs from 'node:fs';
|
|
import path from 'node:path';
|
|
import { chromium } from 'playwright';
|
|
|
|
const root = path.resolve(new URL('..', import.meta.url).pathname);
|
|
const base = 'http://127.0.0.1:7011';
|
|
const owner = 'sft_alex_creator';
|
|
const routingMode = 'recent_model_choice';
|
|
const endpointId = process.env.ENDPOINT_ID || '1d1022ef';
|
|
const endpointUrl = process.env.ENDPOINT_URL || 'http://100.67.207.85:19184/v1/chat/completions';
|
|
const fixture = path.resolve(process.env.FIXTURE_PATH || path.join(root, 'tests/fixtures/vl/basic-shapes.png'));
|
|
const reportPath = process.env.REPORT_PATH
|
|
? path.resolve(process.env.REPORT_PATH)
|
|
: path.join(root, `reports/clean-v3-vl-live-${new Date().toISOString().replace(/[:.]/g, '-')}.json`);
|
|
if (!reportPath.startsWith(path.join(root, 'reports') + path.sep)) throw Error('Report must be under reports/');
|
|
if (fs.existsSync(reportPath)) throw Error('Report exists; refuse overwrite');
|
|
const authSessions = JSON.parse(fs.readFileSync('/home/pewds/odysseus-cookbook-fresh/data/sessions.json'));
|
|
const token = Object.entries(authSessions).find(([, value]) => value?.username === owner)?.[0];
|
|
if (!token) throw Error('Dedicated SFT account has no active auth session');
|
|
const report = { status: 'running', owner, fixture: path.relative(root, fixture), turns: [], checks: {}, cleanup: null };
|
|
const save = () => fs.writeFileSync(reportPath, JSON.stringify(report, null, 2) + '\n');
|
|
let browser, context, session;
|
|
|
|
const parseEvents = async response => (await response.text())
|
|
.split(/\r?\n\r?\n/)
|
|
.filter(line => line.startsWith('data: ') && line.slice(6) !== '[DONE]')
|
|
.map(line => JSON.parse(line.slice(6)));
|
|
|
|
try {
|
|
browser = await chromium.launch({ headless: true, args: ['--no-proxy-server'] });
|
|
context = await browser.newContext({ serviceWorkers: 'block',
|
|
...(process.env.MOBILE === 'true' ? { viewport: { width: 390, height: 844 }, isMobile: true, hasTouch: true } : {}),
|
|
extraHTTPHeaders: { 'x-odysseus-routing-experiment': routingMode } });
|
|
await context.addCookies([{ name: 'odysseus_session', value: token, url: base }]);
|
|
const created = await context.request.post(`${base}/api/session`, { multipart: {
|
|
name: '[clean-v3-vl] basic shapes',
|
|
model: 'odysseus-qwen3.5-tools-pre-heretic',
|
|
endpoint_id: endpointId,
|
|
endpoint_url: endpointUrl,
|
|
skip_validation: 'true', rag: 'false',
|
|
}});
|
|
if (!created.ok()) throw Error(`session create ${created.status()}`);
|
|
session = (await created.json()).id;
|
|
report.session = session; save();
|
|
|
|
const page = await context.newPage();
|
|
await page.goto(`${base}/#${session}`, { waitUntil: 'domcontentloaded' });
|
|
await page.waitForFunction(id => window.__odysseusSessionReadyId === id, session);
|
|
const agent = page.locator('#mode-agent-btn');
|
|
if (await agent.getAttribute('aria-pressed') !== 'true') await agent.click();
|
|
|
|
const send = async prompt => {
|
|
const responsePromise = page.waitForResponse(r => new URL(r.url()).pathname === '/api/chat_stream' && r.request().method() === 'POST', { timeout: 90000 });
|
|
await page.locator('textarea#message:visible').fill(prompt);
|
|
await page.locator('textarea#message:visible').press('Enter');
|
|
const response = await responsePromise;
|
|
const events = await parseEvents(response);
|
|
const text = events.filter(x => typeof x.delta === 'string').map(x => x.delta).join('');
|
|
const final = events.filter(x => x.type === 'final_response').map(x => x.content || '').join('') || text;
|
|
const contract = events.find(x => x.type === 'turn_contract');
|
|
if (contract?.routing_experiment !== routingMode) throw Error('Wrong model-specific runtime');
|
|
const turn = {
|
|
prompt, http: response.status(), selection_mode: contract?.selection_mode,
|
|
image_context_count: contract?.multimodal_image_count,
|
|
image_rehydration: contract?.image_rehydration,
|
|
final, tools: events.filter(x => x.type === 'tool_output').map(x => ({ tool: x.tool, exit_code: x.exit_code, error: x.error })),
|
|
};
|
|
report.turns.push(turn); save();
|
|
return turn;
|
|
};
|
|
|
|
await page.locator('#file-input').setInputFiles(fixture);
|
|
if (process.env.MOBILE === 'true') {
|
|
// Mobile intentionally asks the user to crop or keep the original first.
|
|
await page.locator('.attach-crop-overlay [data-action="original"]').click();
|
|
}
|
|
await page.locator('#attach-strip .thumb').waitFor({ state: 'visible' }).catch(async error => {
|
|
report.attachment_diagnostics = await page.evaluate(() => ({
|
|
strip_count: document.querySelectorAll('#attach-strip').length,
|
|
thumb_count: document.querySelectorAll('#attach-strip .thumb').length,
|
|
strip_display: document.querySelector('#attach-strip') && getComputedStyle(document.querySelector('#attach-strip')).display,
|
|
body_classes: document.body.className,
|
|
}));
|
|
await page.screenshot({ path: reportPath.replace(/\.json$/, '.png') });
|
|
throw error;
|
|
});
|
|
const first = await send('Read the image. State the exact heading and describe the left and right shapes with their colors.');
|
|
const firstText = first.final.toLowerCase();
|
|
if (first.selection_mode !== 'clean_compact_v3_preview') throw Error('First turn did not use clean v3');
|
|
report.checks.first_turn_route = true;
|
|
report.checks.ocr = firstText.includes('odysseus 42');
|
|
report.checks.visual_objects = ['red', 'circle', 'blue', 'square'].every(required => firstText.includes(required));
|
|
if (!report.checks.visual_objects) throw Error('First answer missed one or more visual objects');
|
|
|
|
await page.reload({ waitUntil: 'domcontentloaded' });
|
|
await page.waitForFunction(id => window.__odysseusSessionReadyId === id, session);
|
|
await page.waitForFunction(() => document.querySelectorAll('#chat-history .msg').length >= 2);
|
|
const second = await send('What color was the shape on the right?');
|
|
if (second.selection_mode !== 'clean_compact_v3_preview') throw Error('Follow-up did not use clean v3');
|
|
report.checks.reload_followup_route = true;
|
|
report.checks.reload_followup_grounding = /\bblue\b/i.test(second.final);
|
|
if (!report.checks.reload_followup_grounding) throw Error('Image follow-up was not grounded in the prior image');
|
|
const third = await send('Look at the original image again very carefully. What exact letters and number are in the heading?');
|
|
if (third.selection_mode !== 'clean_compact_v3_preview') throw Error('OCR retry did not use clean v3');
|
|
report.checks.ocr_retry_route = true;
|
|
report.checks.ocr_retry_grounding = /odysseus\s*42/i.test(third.final);
|
|
const fourth = await send('Use the OCR tool to extract the heading text from the attached image, not its filename.');
|
|
report.checks.explicit_ocr_called = fourth.tools.some(tool => tool.tool === 'extract_text' && tool.exit_code === 0);
|
|
report.checks.explicit_ocr_grounding = /odysseus\s*42/i.test(fourth.final);
|
|
const fifth = await send('Run OCR on that same image again, but return only the number this time.');
|
|
report.checks.numeric_ocr_called = fifth.tools.some(tool => tool.tool === 'extract_text' && tool.exit_code === 0);
|
|
report.checks.numeric_ocr_grounding = /\b42\b/.test(fifth.final);
|
|
report.checks.no_tool_errors = report.turns.every(turn => turn.tools.every(tool => !tool.error && tool.exit_code === 0));
|
|
report.status = Object.values(report.checks).every(Boolean) ? 'passed' : 'partial';
|
|
} catch (error) {
|
|
report.status = 'failed';
|
|
report.error = `${error.name}: ${error.message}`;
|
|
} finally {
|
|
if (session && context) {
|
|
const removed = await context.request.delete(`${base}/api/session/${encodeURIComponent(session)}`);
|
|
report.cleanup = { session, status: removed.status(), removed: removed.ok() };
|
|
if (!report.cleanup.removed) report.status = 'failed';
|
|
}
|
|
save();
|
|
if (browser) await browser.close();
|
|
}
|
|
|
|
console.log(JSON.stringify({ status: report.status, turns: report.turns.map(t => ({ mode: t.selection_mode, final: t.final })), cleanup: report.cleanup, error: report.error }));
|
|
if (report.status !== 'passed') process.exitCode = 1;
|