testronaut 1.11.0 → 1.12.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +226 -0
- package/TODO.md +5 -0
- package/bin/cli.js +232 -5
- package/bin/terminalUi.js +9 -1
- package/core/agent.js +7 -2
- package/core/browserEvidence.js +184 -0
- package/core/jevRouting.js +77 -0
- package/core/jevShadow.js +325 -0
- package/core/jevSuiteSelection.js +39 -0
- package/core/jevUsage.js +62 -0
- package/core/missionDiscovery.js +56 -6
- package/core/optimization.js +98 -0
- package/core/turnLoop.js +232 -11
- package/experiments/CROSS_PROVIDER.md +19 -0
- package/experiments/README.md +62 -0
- package/experiments/buildExperimentPublicationPackage.js +219 -0
- package/experiments/buildOptimizationDataPackage.js +390 -0
- package/experiments/crossProviderBenchmark.js +227 -0
- package/experiments/optimizationBenchmark.js +339 -0
- package/package.json +5 -1
- package/tests/binTests/cli.helpers.test.js +16 -0
- package/tests/binTests/cli.integration.test.js +18 -1
- package/tests/binTests/terminalUi.test.js +2 -1
- package/tests/coreTests/agent.test.js +2 -0
- package/tests/coreTests/browserEvidence.test.js +70 -0
- package/tests/coreTests/jevRouting.test.js +57 -0
- package/tests/coreTests/jevShadow.test.js +158 -0
- package/tests/coreTests/jevSuiteSelection.test.js +24 -0
- package/tests/coreTests/jevUsage.test.js +58 -0
- package/tests/coreTests/missionDiscovery.test.js +44 -0
- package/tests/coreTests/optimization.test.js +80 -0
- package/tests/coreTests/turnLoop.test.js +351 -1
- package/tests/experimentTests/buildOptimizationDataPackage.test.js +25 -0
- package/tests/experimentTests/crossProviderBenchmark.test.js +37 -0
- package/tests/experimentTests/optimizationBenchmark.test.js +49 -0
- package/tests/toolsTests/generateHtmlReport.test.js +100 -0
- package/tools/generateHtmlReport.js +67 -10
|
@@ -18,6 +18,7 @@
|
|
|
18
18
|
import fs from 'fs';
|
|
19
19
|
import path from 'path';
|
|
20
20
|
import { normalizeTags } from '../core/tags.js';
|
|
21
|
+
import { summarizeJevUsage } from '../core/jevUsage.js';
|
|
21
22
|
|
|
22
23
|
/**
|
|
23
24
|
* Render and write a Testronaut run report to disk.
|
|
@@ -27,10 +28,11 @@ import { normalizeTags } from '../core/tags.js';
|
|
|
27
28
|
* @returns {string} absolute path to the written HTML file
|
|
28
29
|
*/
|
|
29
30
|
export function generateHtmlReport(report, outputPath) {
|
|
30
|
-
const { runId, startTime, endTime, missions = [], summary = {}, llm = {}, cli = {} } = report;
|
|
31
|
+
const { runId, startTime, endTime, missions = [], summary = {}, llm = {}, cli = {}, optimization } = report;
|
|
31
32
|
const reportTags = normalizeTags(report.tags ?? missions.flatMap(m => m.submissionType === 'mission' ? (m.tags ?? []) : []));
|
|
32
|
-
const durationSec =
|
|
33
|
-
(
|
|
33
|
+
const durationSec = Number.isFinite(report.durationMs)
|
|
34
|
+
? (report.durationMs / 1000).toFixed(2)
|
|
35
|
+
: (startTime && endTime)
|
|
34
36
|
? ((new Date(endTime) - new Date(startTime)) / 1000).toFixed(2)
|
|
35
37
|
: '—';
|
|
36
38
|
|
|
@@ -47,6 +49,7 @@ export function generateHtmlReport(report, outputPath) {
|
|
|
47
49
|
: String(llm.provider ?? '').trim().toLowerCase();
|
|
48
50
|
const provider = {
|
|
49
51
|
openai: { name: 'OpenAI', logo: '<svg viewBox="0 0 24 24" aria-hidden="true"><path fill="currentColor" d="M9.205 8.658v-2.26c0-.19.072-.333.238-.428l4.543-2.616c.619-.357 1.356-.523 2.117-.523 2.854 0 4.662 2.212 4.662 4.566 0 .167 0 .357-.024.547l-4.71-2.759a.797.797 0 00-.856 0l-5.97 3.473zm10.609 8.8V12.06c0-.333-.143-.57-.429-.737l-5.97-3.473 1.95-1.118a.433.433 0 01.476 0l4.543 2.617c1.309.76 2.189 2.378 2.189 3.948 0 1.808-1.07 3.473-2.76 4.163zM7.802 12.703l-1.95-1.142c-.167-.095-.239-.238-.239-.428V5.899c0-2.545 1.95-4.472 4.591-4.472 1 0 1.927.333 2.712.928L8.23 5.067c-.285.166-.428.404-.428.737v6.898zM12 15.128l-2.795-1.57v-3.33L12 8.658l2.795 1.57v3.33L12 15.128zm1.796 7.23c-1 0-1.927-.332-2.712-.927l4.686-2.712c.285-.166.428-.404.428-.737v-6.898l1.974 1.142c.167.095.238.238.238.428v5.233c0 2.545-1.974 4.472-4.614 4.472zm-5.637-5.303l-4.544-2.617c-1.308-.761-2.188-2.378-2.188-3.948A4.482 4.482 0 014.21 6.327v5.423c0 .333.143.571.428.738l5.947 3.449-1.95 1.118a.432.432 0 01-.476 0zm-.262 3.9c-2.688 0-4.662-2.021-4.662-4.519 0-.19.024-.38.047-.57l4.686 2.71c.286.167.571.167.856 0l5.97-3.448v2.26c0 .19-.07.333-.237.428l-4.543 2.616c-.619.357-1.356.523-2.117.523zm5.899 2.83a5.947 5.947 0 005.827-4.756C22.287 18.339 24 15.84 24 13.296c0-1.665-.713-3.282-1.998-4.448.119-.5.19-.999.19-1.498 0-3.401-2.759-5.947-5.946-5.947-.642 0-1.26.095-1.88.31A5.962 5.962 0 0010.205 0a5.947 5.947 0 00-5.827 4.757C1.713 5.447 0 7.945 0 10.49c0 1.666.713 3.283 1.998 4.448-.119.5-.19 1-.19 1.499 0 3.401 2.759 5.946 5.946 5.946.642 0 1.26-.095 1.88-.309a5.96 5.96 0 004.162 1.713z"/></svg>' },
|
|
52
|
+
gemini: { name: 'Google Gemini', logo: '<svg viewBox="0 0 24 24" aria-hidden="true"><defs><linearGradient id="gemini-gradient" x1="4" y1="20" x2="20" y2="4"><stop stop-color="#3186ff"/><stop offset=".5" stop-color="#8e68ff"/><stop offset="1" stop-color="#ef78b5"/></linearGradient></defs><path fill="url(#gemini-gradient)" d="M12 2c.7 5.8 4.2 9.3 10 10-5.8.7-9.3 4.2-10 10-.7-5.8-4.2-9.3-10-10 5.8-.7 9.3-4.2 10-10z"/></svg>' },
|
|
50
53
|
anthropic: { name: 'Anthropic', logo: '<img src="data:image/svg+xml;base64,PHN2ZyByb2xlPSJpbWciIHZpZXdCb3g9IjAgMCAyNCAyNCIgeG1sbnM9Imh0dHA6Ly93d3cudzMub3JnLzIwMDAvc3ZnIj4KICA8dGl0bGU+Q2xhdWRlPC90aXRsZT4KICA8cmVjdCB3aWR0aD0iMjQiIGhlaWdodD0iMjQiIHJ4PSI1IiBmaWxsPSIjRDk3NzU3Ii8+CiAgPHBhdGggZmlsbD0iI2ZmZiIgdHJhbnNmb3JtPSJ0cmFuc2xhdGUoMyAzKSBzY2FsZSguNzUpIiBkPSJNNC43MDkgMTUuOTU1bDQuNzItMi42NDcuMDgtLjIzLS4wOC0uMTI4SDkuMmwtLjc5LS4wNDgtMi42OTgtLjA3My0yLjMzOS0uMDk3LTIuMjY2LS4xMjItLjU3MS0uMTIxTDAgMTEuNzg0bC4wNTUtLjM1Mi40OC0uMzIxLjY4Ni4wNiAxLjUyLjEwMyAyLjI3OC4xNTggMS42NTIuMDk3IDIuNDQ5LjI1NWguMzg5bC4wNTUtLjE1Ny0uMTM0LS4wOTgtLjEwMy0uMDk3LTIuMzU4LTEuNTk2LTIuNTUyLTEuNjg4LTEuMzM2LS45NzItLjcyNC0uNDkxLS4zNjQtLjQ2Mi0uMTU4LTEuMDA4LjY1Ni0uNzIyLjg4MS4wNi4yMjUuMDYxLjg5My42ODYgMS45MDggMS40NzYgMi40OTEgMS44MzMuMzY1LjMwNC4xNDUtLjEwMy4wMTktLjA3My0uMTY0LS4yNzQtMS4zNTUtMi40NDYtMS40NDYtMi40OS0uNjQ0LTEuMDMyLS4xNy0uNjE5YTIuOTcgMi45NyAwIDAxLS4xMDQtLjcyOUw2LjI4My4xMzQgNi42OTYgMGwuOTk2LjEzNC40Mi4zNjQuNjIgMS40MTQgMS4wMDIgMi4yMjkgMS41NTUgMy4wMy40NTYuODk4LjI0My44MzIuMDkxLjI1NWguMTU4VjkuMDFsLjEyOC0xLjcwNi4yMzctMi4wOTUuMjMtMi42OTUuMDgtLjc2LjM3Ni0uOTEuNzQ3LS40OTIuNTg0LjI4LjQ4LjY4NS0uMDY3LjQ0NC0uMjg2IDEuODUxLS41NTkgMi45MDMtLjM2NCAxLjk0MmguMjEybC4yNDMtLjI0Mi45ODUtMS4zMDYgMS42NTItMi4wNjQuNzMtLjgyLjg1LS45MDQuNTQ3LS40MzFoMS4wMzNsLjc2IDEuMTI5LS4zNCAxLjE2Ni0xLjA2NCAxLjM0Ny0uODgxIDEuMTQyLTEuMjY0IDEuNy0uNzkgMS4zNi4wNzMuMTEuMTg4LS4wMiAyLjg1Ni0uNjA2IDEuNTQzLS4yOCAxLjg0MS0uMzE1LjgzMy4zODguMDkxLjM5NS0uMzI4LjgwNy0xLjk2OS40ODYtMi4zMDkuNDYyLTMuNDM5LjgxMy0uMDQyLjAzLjA0OS4wNjEgMS41NDkuMTQ2LjY2Mi4wMzZoMS42MjJsMy4wMi4yMjUuNzkuNTIyLjQ3NC42MzgtLjA3OS40ODUtMS4yMTUuNjItMS42NC0uMzg5LTMuODI5LS45MS0xLjMxMi0uMzI5aC0uMTgydi4xMWwxLjA5MyAxLjA2OCAyLjAwNiAxLjgxIDIuNTA5IDIuMzMuMTI3LjU3OC0uMzIyLjQ1NS0uMzQtLjA0OS0yLjIwNS0xLjY1Ny0uODUxLS43NDctMS45MjYtMS42MmgtLjEyOHYuMTdsLjQ0NC42NDkgMi4zNDUgMy41MjEuMTIyIDEuMDgtLjE3LjM1My0uNjA4LjIxMy0uNjY4LS4xMjItMS4zNzQtMS45MjUtMS40MTUtMi4xNjctMS4xNDMtMS45NDMtLjE0LjA4LS42NzQgNy4yNTQtLjMxNi4zNy0uNzI5LjI4LS42MDctLjQ2MS0uMzIyLS43NDcuMzIyLTEuNDc2LjM4OS0xLjkyNC4zMTUtMS41My4yODYtMS45LjE3LS42MzItLjAxMi0uMDQyLS4xNC4wMTgtMS40MzQgMS45NjctMi4xOCAyLjk0NS0xLjcyNiAxLjg0NS0uNDE0LjE2NC0uNzE3LS4zNy4wNjctLjY2Mi40MDEtLjU4OSAyLjM4OC0zLjAzNiAxLjQ0LTEuODgyLjkzLTEuMDg2LS4wMDYtLjE1OGgtLjA1NUw0LjEzMiAxOC41NmwtMS4xMy4xNDYtLjQ4Ny0uNDU2LjA2MS0uNzQ2LjIzMS0uMjQzIDEuOTA4LTEuMzEyLS4wMDYuMDA2eiIvPgo8L3N2Zz4K" alt="" />' },
|
|
51
54
|
}[providerKey] ?? { name: llm.provider || 'Unknown provider', logo: '' };
|
|
52
55
|
const tagPalette = [
|
|
@@ -59,14 +62,45 @@ export function generateHtmlReport(report, outputPath) {
|
|
|
59
62
|
const [color, background] = tagPalette[index];
|
|
60
63
|
return `color:${color};background:${background};border-color:${color}66`;
|
|
61
64
|
};
|
|
65
|
+
const formatTokens = value => Number(value || 0).toLocaleString('en-US');
|
|
66
|
+
const summarizeModelTokens = (steps) => steps.reduce((usage, step) => {
|
|
67
|
+
const input = Number(step.inputTokens || 0);
|
|
68
|
+
const output = Number(step.outputTokens || 0);
|
|
69
|
+
const model = String(step.model || llm.model || 'Model');
|
|
70
|
+
usage.input += input;
|
|
71
|
+
usage.output += output;
|
|
72
|
+
usage.total += Number(step.tokensUsed ?? (input + output));
|
|
73
|
+
usage.byModel[model] ||= { input: 0, output: 0, total: 0, turns: 0 };
|
|
74
|
+
usage.byModel[model].input += input;
|
|
75
|
+
usage.byModel[model].output += output;
|
|
76
|
+
usage.byModel[model].total += Number(step.tokensUsed ?? (input + output));
|
|
77
|
+
usage.byModel[model].turns += 1;
|
|
78
|
+
return usage;
|
|
79
|
+
}, { input: 0, output: 0, total: 0, byModel: {} });
|
|
80
|
+
const tokenMeta = (modelUsage, jevUsage) => [
|
|
81
|
+
`Model tokens: ${formatTokens(modelUsage.total)} (${formatTokens(modelUsage.input)} in / ${formatTokens(modelUsage.output)} out)`,
|
|
82
|
+
Object.keys(modelUsage.byModel).length > 1
|
|
83
|
+
? `By model: ${Object.entries(modelUsage.byModel).map(([model, usage]) => `${model} ${formatTokens(usage.total)}`).join(' / ')}`
|
|
84
|
+
: '',
|
|
85
|
+
jevUsage?.completed ? `Jev tokens: ${formatTokens(jevUsage.inputTokens)} in / ${formatTokens(jevUsage.outputTokens)} out` : '',
|
|
86
|
+
].filter(Boolean).join(' • ');
|
|
87
|
+
const reportJevUsage = report.jevShadow?.evaluations
|
|
88
|
+
? report.jevShadow
|
|
89
|
+
: summarizeJevUsage(missions.flatMap(m => Array.isArray(m.steps) ? m.steps : []));
|
|
90
|
+
const reportModelUsage = summarizeModelTokens(missions.flatMap(m => Array.isArray(m.steps) ? m.steps : []));
|
|
91
|
+
const reportModels = Object.keys(reportModelUsage.byModel);
|
|
92
|
+
const jevLogo = '<svg viewBox="0 0 24 24" role="img" aria-hidden="true"><rect width="24" height="24" rx="4" fill="#e547b5"/><g fill="none" stroke="#181a1b" stroke-width="2.25" stroke-linecap="square" stroke-linejoin="round"><path d="M12 2.5 6.1 6v10.1l3.2 1.9 2.7-1.6V9.6l3-1.8V4.3z"/><path d="m9.3 9.6 2.7 1.6 3-1.8 3 1.8v7L12 21.5l-3-1.8V13z"/><path d="m12 11.2 3 1.8 3-1.8M15 13v5.2l-6 3.3M9.3 9.6 12 8l3 1.8"/></g></svg>';
|
|
62
93
|
|
|
63
94
|
const submissionBlock = (m) => {
|
|
64
|
-
const mDurationSec =
|
|
65
|
-
m.
|
|
95
|
+
const mDurationSec = Number.isFinite(m.durationMs)
|
|
96
|
+
? (m.durationMs / 1000).toFixed(2)
|
|
97
|
+
: m.endTime && m.startTime
|
|
66
98
|
? ((m.endTime - m.startTime) / 1000).toFixed(2)
|
|
67
99
|
: (typeof m.durationSeconds === 'number' ? m.durationSeconds.toFixed(2) : '—');
|
|
68
100
|
|
|
69
101
|
const steps = Array.isArray(m.steps) ? m.steps : [];
|
|
102
|
+
const missionJevUsage = m.jevShadow?.evaluations ? m.jevShadow : summarizeJevUsage(steps);
|
|
103
|
+
const missionModelUsage = summarizeModelTokens(steps);
|
|
70
104
|
const stepItems = steps.map((step, idx) => {
|
|
71
105
|
const events = Array.isArray(step.events) ? step.events : [];
|
|
72
106
|
const ok = /✅|Passed|Mission Success/i.test(step.result || '');
|
|
@@ -87,6 +121,19 @@ export function generateHtmlReport(report, outputPath) {
|
|
|
87
121
|
const imgTag = step.screenshotPath
|
|
88
122
|
? `<img src="${esc(step.screenshotPath)}" alt="screenshot turn ${esc(step.turn ?? idx)}">`
|
|
89
123
|
: '';
|
|
124
|
+
const stepJev = step.jevShadow?.status === 'ok'
|
|
125
|
+
? ` • Jev: ${formatTokens(step.jevShadow.usage?.input_tokens)} in / ${formatTokens(step.jevShadow.usage?.output_tokens)} out`
|
|
126
|
+
: step.jevShadow
|
|
127
|
+
? ` • Jev: ${esc(step.jevShadow.status)}${step.jevShadow.reason ? ` (${esc(step.jevShadow.reason)})` : ''}`
|
|
128
|
+
: '';
|
|
129
|
+
const routeLabel = step.modelRouting?.fallback
|
|
130
|
+
? ' • Jev route fallback'
|
|
131
|
+
: step.modelRouting?.applied
|
|
132
|
+
? ' • routed by Jev'
|
|
133
|
+
: '';
|
|
134
|
+
const stepDuration = Number.isFinite(step.durationMs)
|
|
135
|
+
? ` • ${(step.durationMs / 1000).toFixed(2)}s`
|
|
136
|
+
: '';
|
|
90
137
|
|
|
91
138
|
const plan = (typeof step.summary === 'string' && step.summary.trim())
|
|
92
139
|
? step.summary.trim()
|
|
@@ -98,11 +145,11 @@ export function generateHtmlReport(report, outputPath) {
|
|
|
98
145
|
return `
|
|
99
146
|
<details class="step" ${ok ? '' : 'open'}>
|
|
100
147
|
<summary>
|
|
101
|
-
<span class="turn"
|
|
148
|
+
<span class="turn">${step.isClosureTurn ? 'Closure' : `Turn ${esc((step.turn ?? idx) + 1)}`}${esc(attemptLabel)}${stepDuration}</span>
|
|
102
149
|
${humanInput}
|
|
103
150
|
${planSpan}
|
|
104
151
|
<span class="step-result ${ok ? 'ok' : 'bad'}" ${resultTooltip ? `title="${esc(resultTooltip)}"` : ''}>${esc(resultRaw)}</span>
|
|
105
|
-
<span class="tokens"
|
|
152
|
+
<span class="tokens">${esc(step.model || llm.model || 'Model')} tokens: ${esc(step.tokensUsed ?? '—')} (${esc(step.inputTokens ?? '—')} in / ${esc(step.outputTokens ?? '—')} out)${routeLabel}${stepJev}</span>
|
|
106
153
|
</summary>
|
|
107
154
|
<pre class="events">${esc(events.join('\n')) || '(no events)'}</pre>
|
|
108
155
|
${imgTag}
|
|
@@ -120,7 +167,7 @@ export function generateHtmlReport(report, outputPath) {
|
|
|
120
167
|
<summary>
|
|
121
168
|
<span class="name">${prettyTitle}</span>
|
|
122
169
|
<span class="status ${statusClass}">${badge(m.status)}</span>
|
|
123
|
-
<span class="meta">steps: ${steps.length} • duration: ${mDurationSec}s</span>
|
|
170
|
+
<span class="meta">steps: ${steps.length} • duration: ${mDurationSec}s • ${esc(tokenMeta(missionModelUsage, missionJevUsage))}</span>
|
|
124
171
|
<span class="toolbar">
|
|
125
172
|
<button class="btn-mini toggle" data-scope="submission" aria-label="Expand">▼</button>
|
|
126
173
|
</span>
|
|
@@ -151,6 +198,8 @@ export function generateHtmlReport(report, outputPath) {
|
|
|
151
198
|
const groupDur = (firstStart != null && lastEnd != null) ? ((lastEnd - firstStart) / 1000).toFixed(2) : '—';
|
|
152
199
|
const tags = normalizeTags(subs.flatMap(s => s.submissionType === 'mission' ? (s.tags ?? []) : []));
|
|
153
200
|
const sourceFiles = [...new Set(subs.map(s => s.file).filter(Boolean))];
|
|
201
|
+
const groupJevUsage = summarizeJevUsage(subs.flatMap(s => Array.isArray(s.steps) ? s.steps : []));
|
|
202
|
+
const groupModelUsage = summarizeModelTokens(subs.flatMap(s => Array.isArray(s.steps) ? s.steps : []));
|
|
154
203
|
|
|
155
204
|
// pre → mission → post
|
|
156
205
|
const order = { premission: 0, mission: 1, postmission: 2 };
|
|
@@ -165,7 +214,7 @@ export function generateHtmlReport(report, outputPath) {
|
|
|
165
214
|
<span class="name">${esc(missionName)}</span>
|
|
166
215
|
<span class="mission-tags">${tags.map(tag => `<span class="tag-small" style="${tagStyle(tag)}">${esc(tag)}</span>`).join('')}</span>
|
|
167
216
|
<span class="status ${status === 'failed' ? 'bad' : 'ok'}">${badge(status)}</span>
|
|
168
|
-
<span class="meta">${sourceFiles.length ? `${sourceFiles.map(esc).join(', ')} • ` : ''}submissions: ${subs.length} • steps: ${totalSteps} • duration: ${groupDur}s</span>
|
|
217
|
+
<span class="meta">${sourceFiles.length ? `${sourceFiles.map(esc).join(', ')} • ` : ''}submissions: ${subs.length} • steps: ${totalSteps} • duration: ${groupDur}s • ${esc(tokenMeta(groupModelUsage, groupJevUsage))}</span>
|
|
169
218
|
<span class="toolbar">
|
|
170
219
|
<button class="btn-mini toggle" data-scope="mission" aria-label="Expand">▼</button>
|
|
171
220
|
</span>
|
|
@@ -277,6 +326,7 @@ export function generateHtmlReport(report, outputPath) {
|
|
|
277
326
|
.pill:first-child{border-color:rgba(255,138,67,.5);box-shadow:inset 3px 0 var(--orange);}
|
|
278
327
|
.pill.ok{ background: var(--chip-ok-bg); color: var(--ok); border-color: var(--chip-ok-border); font-weight:700; }
|
|
279
328
|
.pill.bad{ background: var(--chip-bad-bg); color: var(--bad); border-color: var(--chip-bad-border); font-weight:700; }
|
|
329
|
+
.pill.warn{ background:rgba(251,191,36,.14);color:#fbbf24;border-color:rgba(251,191,36,.4);font-weight:700; }
|
|
280
330
|
.tag-filter{max-width:928px;margin:0 auto 20px;padding:18px 16px;background:rgba(2,6,23,.30);}
|
|
281
331
|
.tag-filter-heading{display:flex;align-items:center;gap:7px;margin-bottom:12px;flex-wrap:wrap;}
|
|
282
332
|
.tag-filter-title{font-size:14px;color:var(--text);font-weight:800;}
|
|
@@ -377,7 +427,7 @@ export function generateHtmlReport(report, outputPath) {
|
|
|
377
427
|
<div class="eyebrow">Mission Control · Flight Report</div>
|
|
378
428
|
<h1>🧑🚀 <span class="gradient-text">Testronaut Report</span></h1>
|
|
379
429
|
<div class="run-meta">
|
|
380
|
-
<div class="provider-meta"><span class="provider-logo" title="${esc(provider.name)}" aria-label="${esc(provider.name)}">${provider.logo}</span><strong>${esc(llm.model ?? '—')}</strong
|
|
430
|
+
<div class="provider-meta"><span class="provider-logo" title="${esc(provider.name)}" aria-label="${esc(provider.name)}">${provider.logo}</span><strong>${esc((reportModels.length ? reportModels : [llm.model ?? '—']).join(' + '))}</strong>${reportJevUsage.evaluations ? ` <span aria-hidden="true">+</span> <span class="provider-logo" title="Jev" aria-label="Jev">${jevLogo}</span><strong>Jev</strong>` : ''}</div>
|
|
381
431
|
<div><strong>Run ID:</strong> ${esc(runId ?? '—')}</div>
|
|
382
432
|
<div><strong>Start:</strong> ${esc(startTime ?? '—')}</div>
|
|
383
433
|
<div><strong>End:</strong> ${esc(endTime ?? '—')}</div>
|
|
@@ -390,6 +440,13 @@ export function generateHtmlReport(report, outputPath) {
|
|
|
390
440
|
<div class="pill">Missions: ${esc(summary.totalMissions ?? totals.total)}</div>
|
|
391
441
|
<div class="pill ok">Passed: ${esc(summary.passed ?? totals.passed)}</div>
|
|
392
442
|
<div class="pill bad">Failed: ${esc(summary.failed ?? totals.failed)}</div>
|
|
443
|
+
${optimization?.configured ? `<div class="pill">Optimization: ${esc(optimization.objective)} (${esc(optimization.mode)})</div>` : ''}
|
|
444
|
+
${Object.entries(reportModelUsage.byModel).length > 1
|
|
445
|
+
? Object.entries(reportModelUsage.byModel).map(([model, usage]) => `<div class="pill">${esc(model)} tokens: ${esc(formatTokens(usage.total))} (${esc(formatTokens(usage.input))} in / ${esc(formatTokens(usage.output))} out)</div>`).join('')
|
|
446
|
+
: `<div class="pill">Model tokens: ${esc(formatTokens(reportModelUsage.total))} (${esc(formatTokens(reportModelUsage.input))} in / ${esc(formatTokens(reportModelUsage.output))} out)</div>`}
|
|
447
|
+
${reportJevUsage.completed ? `<div class="pill">Jev tokens: ${esc(formatTokens(reportJevUsage.inputTokens))} in / ${esc(formatTokens(reportJevUsage.outputTokens))} out</div>` : ''}
|
|
448
|
+
${reportJevUsage.skipped ? `<div class="pill warn">Jev skipped: ${esc(formatTokens(reportJevUsage.skipped))}</div>` : ''}
|
|
449
|
+
${reportJevUsage.errors ? `<div class="pill warn">Jev errors: ${esc(formatTokens(reportJevUsage.errors))}</div>` : ''}
|
|
393
450
|
</div>
|
|
394
451
|
|
|
395
452
|
<div class="tag-filter glass">
|