/* * verify-matrix.mjs * Zyquo Cloud Web * * Author: Simon-Pierre Boucher * Mail: contact@spboucher.ai * * Phase 7 provider matrix: drives the app's own provider layer inside a real * Chromium page (dev harness) with the user's real keys. Per model: streaming * completion (token deltas, reasoning, usage, finish). Per provider: * non-streaming completion, Stop/cancellation, vision probe (1×1 PNG on the * cheapest vision model), Perplexity citations. Also audits every network * origin the page contacted. Writes /tmp/zyquo-verify.json. * * Usage: node scripts/verify-matrix.mjs [baseURL] [providers…] */ import { chromium } from 'playwright' import { readFileSync, writeFileSync } from 'node:fs' const baseURL = process.argv[2] ?? 'http://localhost:5173' const onlyProviders = process.argv.slice(3) const keys = JSON.parse(readFileSync(new URL('../.keys.local.json', import.meta.url), 'utf8')) // Deliberately skipped models (billing/time-outsized for a smoke matrix). const SKIP = new Set(['sonar-deep-research']) const PER_MODEL_TIMEOUT = 120_000 const browser = await chromium.launch() const page = await browser.newPage() const origins = new Set() page.on('request', (req) => origins.add(new URL(req.url()).origin)) page.setDefaultTimeout(PER_MODEL_TIMEOUT + 10_000) await page.goto(baseURL) await page.evaluate((keyMap) => { localStorage.setItem('zyquo.cloud.web.keys', JSON.stringify(keyMap)) }, keys) await page.reload() await page.waitForFunction(() => window.__zyquo !== undefined) // A guaranteed-valid test image: 128×128 solid red, generated in-page. const PNG_RED = await page.evaluate(() => { const canvas = document.createElement('canvas') canvas.width = 128 canvas.height = 128 const ctx = canvas.getContext('2d') ctx.fillStyle = '#d62828' ctx.fillRect(0, 0, 128, 128) return canvas.toDataURL('image/png').split(',')[1] }) /** Runs one streaming check inside the page. */ async function streamCheck(provider, modelID, opts = {}) { return page.evaluate( async ([provider, modelID, opts, png]) => { const z = window.__zyquo const model = z.findModel(provider, modelID) if (!model) return { ok: false, error: 'not in catalog' } const key = z.keys.getKey(provider) const client = z.clientFor(model) const controller = new AbortController() const start = performance.now() const result = { ok: false, text: 0, reasoning: 0, usage: null, finish: null, citations: 0, deltas: 0, error: null, ms: 0, } const attachments = opts.vision ? [{ id: 'v1', kind: 'image', fileName: 'dot.png', mimeType: 'image/png', data: png }] : [] const timer = setTimeout(() => controller.abort(), opts.timeout ?? 110000) try { const events = client.streamChat( { model, messages: [{ id: 'm1', role: 'user', text: opts.vision ? 'What color is this image? One word.' : (opts.prompt ?? 'Say OK.'), attachments, createdAt: Date.now(), }], parameters: { maxTokens: opts.maxTokens ?? 64 }, stream: true, }, key, controller.signal ) for await (const event of events) { result.deltas++ if (event.type === 'textDelta') result.text += event.text.length else if (event.type === 'reasoningDelta') result.reasoning += event.text.length else if (event.type === 'usage') result.usage = event.usage else if (event.type === 'citations') result.citations = event.citations.length else if (event.type === 'finished') result.finish = event.reason } result.ok = result.text > 0 || result.reasoning > 0 if (!result.ok) result.error = 'stream completed with no content' } catch (err) { result.error = err && err.message ? err.message : String(err) } clearTimeout(timer) result.ms = Math.round(performance.now() - start) return result }, [provider, modelID, opts, PNG_RED] ) } async function completeCheck(provider, modelID) { return page.evaluate( async ([provider, modelID]) => { const z = window.__zyquo const model = z.findModel(provider, modelID) const key = z.keys.getKey(provider) try { const start = performance.now() const result = await z.clientFor(model).complete( { model, messages: [{ id: 'm1', role: 'user', text: 'Reply with exactly: OK', createdAt: Date.now() }], parameters: { maxTokens: 16 }, stream: false, }, key ) return { ok: result.text.length > 0, ms: Math.round(performance.now() - start) } } catch (err) { return { ok: false, error: err && err.message ? err.message : String(err) } } }, [provider, modelID] ) } async function cancelCheck(provider, modelID) { return page.evaluate( async ([provider, modelID]) => { const z = window.__zyquo const model = z.findModel(provider, modelID) const key = z.keys.getKey(provider) const controller = new AbortController() setTimeout(() => controller.abort(), 900) try { const events = z.clientFor(model).streamChat( { model, messages: [{ id: 'm1', role: 'user', text: 'Write a 500-word essay about the sea.', createdAt: Date.now() }], parameters: { maxTokens: 800 }, stream: true, }, key, controller.signal ) for await (const _event of events) { /* drain until abort */ } return { ok: false, error: 'stream finished before abort' } } catch (err) { const message = err && err.message ? err.message : String(err) return { ok: /stopped|cancel/i.test(message), error: message } } }, [provider, modelID] ) } const catalog = await page.evaluate(() => window.__zyquo.CATALOG.map((m) => ({ id: m.id, provider: m.provider, vision: m.capabilities.vision, reasoning: m.capabilities.reasoning, citations: m.capabilities.citations, legacy: m.isLegacy, outPrice: m.pricing ? m.pricing.outputPerMTok : null, })) ) const providers = [...new Set(catalog.map((m) => m.provider))].filter( (p) => onlyProviders.length === 0 || onlyProviders.includes(p) ) const report = { startedAt: new Date().toISOString(), providers: {} } for (const provider of providers) { const models = catalog.filter((m) => m.provider === provider) const entry = { models: {}, nonStreaming: null, cancel: null, vision: null, citations: null } report.providers[provider] = entry console.log(`\n=== ${provider} (${models.length} models) ===`) for (const model of models) { if (SKIP.has(model.id)) { entry.models[model.id] = { skipped: 'expensive deep-research model' } console.log(` SKIP ${model.id}`) continue } const result = await streamCheck(provider, model.id, { maxTokens: model.reasoning ? 256 : 64, }) entry.models[model.id] = result console.log( ` ${result.ok ? 'PASS' : 'FAIL'} ${model.id} (${result.ms}ms, text ${result.text}, reas ${result.reasoning}, deltas ${result.deltas}${result.error ? `, err: ${String(result.error).slice(0, 90)}` : ''})` ) } // Provider-level probes on the cheapest working model const working = models.filter((m) => entry.models[m.id] && entry.models[m.id].ok) const cheapest = working.filter((m) => !m.reasoning).sort((a, b) => (a.outPrice ?? 1e9) - (b.outPrice ?? 1e9))[0] ?? working[0] if (cheapest) { entry.nonStreaming = { model: cheapest.id, ...(await completeCheck(provider, cheapest.id)) } console.log(` non-streaming[${cheapest.id}]: ${entry.nonStreaming.ok ? 'PASS' : 'FAIL ' + (entry.nonStreaming.error ?? '')}`) entry.cancel = { model: cheapest.id, ...(await cancelCheck(provider, cheapest.id)) } console.log(` cancel[${cheapest.id}]: ${entry.cancel.ok ? 'PASS' : 'FAIL ' + (entry.cancel.error ?? '')}`) } const visionModel = working .filter((m) => m.vision && !m.legacy) .sort((a, b) => (a.outPrice ?? 1e9) - (b.outPrice ?? 1e9))[0] if (visionModel) { const v = await streamCheck(provider, visionModel.id, { vision: true, maxTokens: 200 }) entry.vision = { model: visionModel.id, ok: v.ok, error: v.error } console.log(` vision[${visionModel.id}]: ${v.ok ? 'PASS' : 'FAIL ' + (v.error ?? '')}`) } if (provider === 'perplexity') { const c = await streamCheck(provider, 'sonar', { prompt: 'What is the tallest building in the world right now?', maxTokens: 128, }) entry.citations = { model: 'sonar', ok: c.citations > 0, count: c.citations } console.log(` citations[sonar]: ${c.citations > 0 ? `PASS (${c.citations})` : 'FAIL'}`) } } report.contactedOrigins = [...origins].sort() report.finishedAt = new Date().toISOString() writeFileSync('/tmp/zyquo-verify.json', JSON.stringify(report, null, 2)) console.log('\nOrigins contacted:', report.contactedOrigins.join(', ')) console.log('Report → /tmp/zyquo-verify.json') await browser.close()