api-batch-tester / api-tester.html
div0-space's picture
tester: add prompt shuffler + local image attachments
07df0cf verified
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>LibraxisAI Responses API Tester</title>
<style>
:root {
--bg: #0a0a0f;
--surface: #12121a;
--surface-hover: #1a1a24;
--border: #2a2a3a;
--accent: #6366f1;
--accent-hover: #818cf8;
--text: #e4e4e7;
--text-dim: #71717a;
--success: #22c55e;
--error: #ef4444;
--warning: #f59e0b;
--gradient: linear-gradient(135deg, #6366f1 0%, #a855f7 50%, #ec4899 100%);
}
* { box-sizing: border-box; margin: 0; padding: 0; }
body {
font-family: -apple-system, BlinkMacSystemFont, 'SF Pro Display', system-ui, sans-serif;
background: var(--bg);
color: var(--text);
min-height: 100vh;
padding: 1rem;
}
/* Header */
.header {
text-align: center;
padding: 1.5rem 0;
border-bottom: 1px solid var(--border);
margin-bottom: 1.5rem;
}
.header h1 {
font-size: 1.75rem;
font-weight: 700;
background: var(--gradient);
-webkit-background-clip: text;
-webkit-text-fill-color: transparent;
background-clip: text;
}
.header p {
color: var(--text-dim);
font-size: 0.875rem;
margin-top: 0.25rem;
}
/* Global Config */
.global-config {
display: flex;
gap: 1rem;
align-items: center;
flex-wrap: wrap;
padding: 1rem;
background: var(--surface);
border: 1px solid var(--border);
border-radius: 12px;
margin-bottom: 1rem;
}
.global-config label {
font-size: 0.75rem;
color: var(--text-dim);
text-transform: uppercase;
letter-spacing: 0.05em;
}
.global-config input[type="password"] {
flex: 1;
min-width: 200px;
padding: 0.5rem 0.75rem;
background: var(--bg);
border: 1px solid var(--border);
border-radius: 6px;
color: var(--text);
font-size: 0.875rem;
}
.mode-toggle {
display: flex;
gap: 0.5rem;
}
.mode-toggle label {
display: flex;
align-items: center;
gap: 0.25rem;
cursor: pointer;
padding: 0.5rem 0.75rem;
border-radius: 6px;
border: 1px solid var(--border);
font-size: 0.875rem;
text-transform: none;
}
.mode-toggle input:checked + span {
color: var(--accent);
}
.mode-toggle input {
display: none;
}
/* Toolbar */
.toolbar {
display: flex;
gap: 0.75rem;
margin-bottom: 1rem;
flex-wrap: wrap;
}
.btn {
padding: 0.625rem 1rem;
border: none;
border-radius: 8px;
font-size: 0.875rem;
font-weight: 500;
cursor: pointer;
transition: all 0.2s;
display: flex;
align-items: center;
gap: 0.5rem;
}
.btn-secondary {
background: var(--surface);
color: var(--text);
border: 1px solid var(--border);
}
.btn-secondary:hover {
background: var(--surface-hover);
border-color: var(--accent);
}
.btn-primary {
background: var(--gradient);
color: white;
font-weight: 700;
padding: 0.75rem 2rem;
font-size: 1rem;
box-shadow: 0 4px 20px rgba(99, 102, 241, 0.3);
}
.btn-primary:hover {
transform: translateY(-1px);
box-shadow: 0 6px 24px rgba(99, 102, 241, 0.4);
}
.btn-primary:disabled {
opacity: 0.5;
cursor: not-allowed;
transform: none;
}
/* Lanes Container */
.lanes-container {
display: flex;
gap: 1rem;
overflow-x: auto;
padding-bottom: 1rem;
}
/* Lane */
.lane {
flex: 0 0 360px;
background: var(--surface);
border: 1px solid var(--border);
border-radius: 12px;
display: flex;
flex-direction: column;
}
.lane-header {
display: flex;
justify-content: space-between;
align-items: center;
padding: 0.75rem 1rem;
border-bottom: 1px solid var(--border);
background: rgba(99, 102, 241, 0.05);
}
.lane-title {
font-weight: 600;
color: var(--accent);
display: flex;
align-items: center;
gap: 0.5rem;
}
.lane-actions {
display: flex;
gap: 0.25rem;
}
.lane-actions button {
background: transparent;
border: none;
color: var(--text-dim);
cursor: pointer;
padding: 0.25rem;
border-radius: 4px;
font-size: 1rem;
}
.lane-actions button:hover {
background: var(--surface-hover);
color: var(--text);
}
.lane-config {
padding: 1rem;
display: flex;
flex-direction: column;
gap: 0.75rem;
border-bottom: 1px solid var(--border);
}
.field {
display: flex;
flex-direction: column;
gap: 0.25rem;
}
.field label {
font-size: 0.75rem;
color: var(--text-dim);
text-transform: uppercase;
letter-spacing: 0.05em;
}
.field input, .field select, .field textarea {
padding: 0.5rem 0.75rem;
background: var(--bg);
border: 1px solid var(--border);
border-radius: 6px;
color: var(--text);
font-size: 0.875rem;
font-family: inherit;
}
.field input:focus, .field select:focus, .field textarea:focus {
outline: none;
border-color: var(--accent);
}
.field textarea {
min-height: 60px;
resize: vertical;
}
.field-row {
display: flex;
gap: 0.5rem;
}
.field-row .field {
flex: 1;
}
/* Prompts Section */
.prompts-section {
padding: 1rem;
border-bottom: 1px solid var(--border);
max-height: 300px;
overflow-y: auto;
}
.prompts-section h4 {
font-size: 0.75rem;
color: var(--text-dim);
text-transform: uppercase;
letter-spacing: 0.05em;
margin-bottom: 0.75rem;
}
.prompt-item {
margin-bottom: 0.75rem;
}
.prompt-item label {
font-size: 0.75rem;
color: var(--accent);
margin-bottom: 0.25rem;
display: block;
}
.prompt-item textarea {
width: 100%;
padding: 0.5rem;
background: var(--bg);
border: 1px solid var(--border);
border-radius: 6px;
color: var(--text);
font-size: 0.8125rem;
font-family: inherit;
min-height: 50px;
resize: vertical;
}
/* Output Section */
.output-section {
flex: 1;
padding: 1rem;
display: flex;
flex-direction: column;
gap: 0.5rem;
min-height: 200px;
max-height: 400px;
overflow-y: auto;
}
.output-section h4 {
font-size: 0.75rem;
color: var(--text-dim);
text-transform: uppercase;
letter-spacing: 0.05em;
}
.output-item {
background: var(--bg);
border: 1px solid var(--border);
border-radius: 8px;
overflow: hidden;
}
.output-item-header {
display: flex;
justify-content: space-between;
align-items: center;
padding: 0.5rem 0.75rem;
background: rgba(255,255,255,0.02);
border-bottom: 1px solid var(--border);
font-size: 0.75rem;
cursor: pointer;
}
.output-item-header:hover {
background: rgba(255,255,255,0.04);
}
.output-step {
color: var(--accent);
font-weight: 600;
}
.output-status {
display: flex;
align-items: center;
gap: 0.5rem;
}
.status-dot {
width: 6px;
height: 6px;
border-radius: 50%;
background: var(--text-dim);
}
.status-dot.running {
background: var(--warning);
animation: pulse 1s infinite;
}
.status-dot.success { background: var(--success); }
.status-dot.error { background: var(--error); }
@keyframes pulse {
0%, 100% { opacity: 1; }
50% { opacity: 0.4; }
}
.output-item-body {
padding: 0.75rem;
font-family: 'SF Mono', 'Fira Code', monospace;
font-size: 0.8125rem;
line-height: 1.5;
white-space: pre-wrap;
word-break: break-word;
max-height: 150px;
overflow-y: auto;
}
.output-item-body.collapsed {
max-height: 40px;
overflow: hidden;
}
/* Stream Preview */
.stream-preview {
font-family: 'SF Mono', monospace;
font-size: 0.75rem;
color: var(--text-dim);
padding: 0.25rem 0.5rem;
background: rgba(99, 102, 241, 0.1);
border-radius: 4px;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
max-width: 200px;
}
/* Stats Bar */
.lane-stats {
padding: 0.75rem 1rem;
background: rgba(99, 102, 241, 0.05);
border-top: 1px solid var(--border);
display: grid;
grid-template-columns: repeat(3, 1fr);
gap: 0.5rem;
font-size: 0.75rem;
}
.stat {
text-align: center;
}
.stat-value {
font-weight: 700;
color: var(--accent);
font-size: 1rem;
font-family: 'SF Mono', monospace;
}
.stat-label {
color: var(--text-dim);
text-transform: uppercase;
letter-spacing: 0.05em;
font-size: 0.625rem;
}
/* Footer */
.footer {
text-align: center;
padding: 1.5rem;
color: var(--text-dim);
font-size: 0.75rem;
border-top: 1px solid var(--border);
margin-top: 2rem;
}
/* Scrollbar */
::-webkit-scrollbar { width: 6px; height: 6px; }
::-webkit-scrollbar-track { background: var(--bg); }
::-webkit-scrollbar-thumb { background: var(--border); border-radius: 3px; }
::-webkit-scrollbar-thumb:hover { background: var(--text-dim); }
/* Endpoint presets */
.endpoint-presets {
display: flex;
gap: 0.25rem;
margin-top: 0.25rem;
}
.endpoint-preset {
display: none;
}
.image-input {
margin-top: 0.35rem;
font-size: 0.8rem;
color: var(--text-dim);
}
.notice {
margin: 0 0 1rem 0;
padding: 0.75rem 1rem;
border: 1px solid var(--border);
border-radius: 8px;
background: rgba(99,102,241,0.07);
color: var(--text-dim);
font-size: 0.875rem;
line-height: 1.4;
}
</style>
</head>
<body>
<div class="header">
<h1>LibraxisAI Responses API Tester</h1>
<p>Multi-lane comparison tool for Responses API endpoints with conversation chaining</p>
</div>
<div class="notice">
BYOK: API keys stay in your browser and are sent only to the endpoints you enter. They are not stored or logged on this Space. For sensitive keys, use the per-lane key fields and consider running with Space auth enabled.
</div>
<div class="global-config">
<div class="field" style="flex: 1;">
<label>Global API Key (fallback for lanes without key)</label>
<input type="password" id="globalApiKey" value="" placeholder="your-api-key-here">
</div>
<div class="field">
<label>Mode</label>
<div class="mode-toggle">
<label>
<input type="radio" name="streamMode" value="true" checked>
<span>Stream</span>
</label>
<label>
<input type="radio" name="streamMode" value="false">
<span>Sync</span>
</label>
</div>
</div>
<div class="field">
<label>Output</label>
<div class="mode-toggle">
<label title="Show raw SSE events as-is">
<input type="radio" name="outputMode" value="raw" checked>
<span>RAW</span>
</label>
<label title="Parse and show only text content">
<input type="radio" name="outputMode" value="parsed">
<span>Parsed</span>
</label>
</div>
</div>
</div>
<div class="toolbar">
<button class="btn btn-secondary" onclick="addLane()">+ Add Lane</button>
<button class="btn btn-secondary" onclick="clearAllOutputs()">Clear Outputs</button>
<button class="btn btn-secondary" onclick="shufflePromptsAcrossLanes()">Prompt Shuffler</button>
<div style="display: flex; align-items: center; gap: 0.25rem; background: var(--surface); padding: 0.25rem 0.5rem; border-radius: 6px; border: 1px solid var(--border);">
<select id="presetSelect" style="background: var(--bg); border: none; color: var(--text); padding: 0.4rem; border-radius: 4px; min-width: 120px;">
<option value="">-- Presets --</option>
</select>
<button class="btn btn-secondary" onclick="loadPreset()" style="padding: 0.4rem 0.6rem;">Load</button>
<button class="btn btn-secondary" onclick="savePreset()" style="padding: 0.4rem 0.6rem;">Save</button>
<button class="btn btn-secondary" onclick="deletePreset()" style="padding: 0.4rem 0.6rem;">Del</button>
</div>
<button class="btn btn-secondary" onclick="exportResults()">Export JSON</button>
<button class="btn btn-secondary" onclick="copyResultsToClipboard()">Copy</button>
<button class="btn btn-secondary" onclick="exportAllLogs()" title="Export all saved test logs">
Logs <span id="logCount" style="background: var(--accent); padding: 0.1rem 0.4rem; border-radius: 4px; font-size: 0.75rem;">0</span>
</button>
<div style="flex: 1;"></div>
<button class="btn btn-primary" id="runBtn" onclick="runAllLanes()">
RUN ALL
</button>
</div>
<div class="lanes-container" id="lanesContainer">
<!-- Lanes will be added here -->
</div>
<div class="footer">
Vibecrafted with AI Agents by VetCoders (c)2026 The LibraxisAI Team
</div>
<script>
// State
let laneCounter = 0;
const lanes = new Map();
// Endpoint presets
const ENDPOINTS = {
'mlx-batch': 'http://localhost:8100/v1/responses',
'api-router': 'http://localhost:8088/v1/responses',
'remote': 'https://api.libraxis.cloud/v1/responses'
};
// History management for Endpoint and Model fields
const HISTORY_KEY = 'api-tester-history';
const MAX_HISTORY = 10;
function getHistory() {
try {
return JSON.parse(localStorage.getItem(HISTORY_KEY)) || { endpoints: [], models: [] };
} catch {
return { endpoints: [], models: [] };
}
}
function saveToHistory(type, value) {
if (!value || !value.trim()) return;
const history = getHistory();
const list = history[type] || [];
// Remove if exists, add to front
const idx = list.indexOf(value);
if (idx > -1) list.splice(idx, 1);
list.unshift(value);
// Keep max items
history[type] = list.slice(0, MAX_HISTORY);
localStorage.setItem(HISTORY_KEY, JSON.stringify(history));
updateHistoryDatalist(type);
}
function updateHistoryDatalist(type) {
const datalist = document.getElementById(`${type}-history`);
if (!datalist) return;
const history = getHistory();
const items = history[type] || [];
datalist.innerHTML = items.map(v => `<option value="${escapeHtml(v)}">`).join('');
}
function escapeHtml(str) {
const div = document.createElement('div');
div.textContent = str;
return div.innerHTML;
}
// Results storage for export
const testResults = new Map();
function collectResults() {
const results = {
timestamp: new Date().toISOString(),
globalConfig: {
apiKey: document.getElementById('globalApiKey').value ? '***' : 'none',
streamMode: document.querySelector('input[name="streamMode"]:checked').value
},
lanes: []
};
lanes.forEach((_, laneId) => {
const laneData = {
id: laneId,
config: {
endpoint: document.getElementById(`${laneId}-endpoint`).value,
model: document.getElementById(`${laneId}-model`).value,
chainLength: document.getElementById(`${laneId}-chain`).value,
systemPrompt: document.getElementById(`${laneId}-system`).value
},
prompts: [],
responses: [],
stats: {
ttft: document.getElementById(`${laneId}-ttft`)?.textContent || '-',
tps: document.getElementById(`${laneId}-tps`)?.textContent || '-',
total: document.getElementById(`${laneId}-total`)?.textContent || '-'
}
};
// Collect prompts
const chainLength = parseInt(laneData.config.chainLength) || 1;
for (let i = 1; i <= chainLength; i++) {
const promptEl = document.getElementById(`${laneId}-prompt-${i}`);
if (promptEl) laneData.prompts.push(promptEl.value);
const outputEl = document.getElementById(`${laneId}-output-${i}`);
if (outputEl) laneData.responses.push(outputEl.textContent);
}
results.lanes.push(laneData);
});
return results;
}
function exportResults() {
const results = collectResults();
const blob = new Blob([JSON.stringify(results, null, 2)], { type: 'application/json' });
const url = URL.createObjectURL(blob);
const a = document.createElement('a');
a.href = url;
a.download = `api-test-${new Date().toISOString().slice(0,19).replace(/:/g,'-')}.json`;
a.click();
URL.revokeObjectURL(url);
}
function copyResultsToClipboard() {
const results = collectResults();
// Format as readable text
let text = `API Test Results - ${results.timestamp}\n`;
text += `Stream Mode: ${results.globalConfig.streamMode}\n\n`;
results.lanes.forEach((lane, idx) => {
text += `═══ Lane ${idx + 1}: ${lane.config.model} ═══\n`;
text += `Endpoint: ${lane.config.endpoint}\n`;
text += `Stats: TTFT=${lane.stats.ttft} | tok/s=${lane.stats.tps} | Total=${lane.stats.total}\n\n`;
lane.prompts.forEach((prompt, i) => {
text += `[Prompt ${i+1}]: ${prompt.slice(0, 100)}${prompt.length > 100 ? '...' : ''}\n`;
text += `[Response ${i+1}]: ${(lane.responses[i] || '').slice(0, 200)}${(lane.responses[i] || '').length > 200 ? '...' : ''}\n\n`;
});
text += '\n';
});
navigator.clipboard.writeText(text).then(() => {
alert('Results copied to clipboard!');
});
}
async function autoSaveLog(results) {
// Auto-download JSON file (no localStorage size limit)
const json = JSON.stringify(results, null, 2);
const blob = new Blob([json], { type: 'application/json' });
const url = URL.createObjectURL(blob);
const a = document.createElement('a');
a.href = url;
a.download = `api-test-${new Date().toISOString().slice(0,19).replace(/:/g,'-')}.json`;
a.click();
URL.revokeObjectURL(url);
// Also keep in localStorage (best-effort, may fail for large results)
try {
const logKey = 'api-tester-logs';
const existingLogs = JSON.parse(localStorage.getItem(logKey) || '[]');
existingLogs.push(results);
if (existingLogs.length > 50) existingLogs.shift();
localStorage.setItem(logKey, JSON.stringify(existingLogs));
} catch (e) {
console.warn('[API Tester] localStorage full, skipping cache:', e.message);
}
console.log(`[API Tester] Auto-saved: ${a.download} (${(json.length / 1024).toFixed(0)} KB)`);
}
function exportAllLogs() {
const logKey = 'api-tester-logs';
const logs = JSON.parse(localStorage.getItem(logKey) || '[]');
if (logs.length === 0) {
alert('No logs to export');
return;
}
const blob = new Blob([JSON.stringify(logs, null, 2)], { type: 'application/json' });
const url = URL.createObjectURL(blob);
const a = document.createElement('a');
a.href = url;
a.download = `api-test-logs-${new Date().toISOString().slice(0,10)}.json`;
a.click();
URL.revokeObjectURL(url);
alert(`Exported ${logs.length} test logs`);
}
function clearLogs() {
if (confirm('Clear all saved test logs?')) {
localStorage.removeItem('api-tester-logs');
alert('Logs cleared');
}
}
function getLogCount() {
const logs = JSON.parse(localStorage.getItem('api-tester-logs') || '[]');
return logs.length;
}
// === PRESETS SYSTEM ===
const PRESETS_KEY = 'api-tester-presets';
function getPresets() {
return JSON.parse(localStorage.getItem(PRESETS_KEY) || '{}');
}
function savePreset() {
const name = prompt('Preset name:', `preset-${Object.keys(getPresets()).length + 1}`);
if (!name || !name.trim()) return;
const lanes = document.querySelectorAll('.lane');
if (lanes.length === 0) {
alert('No lanes to save');
return;
}
const preset = {
created: new Date().toISOString(),
streaming: document.querySelector('input[name="streamMode"]:checked')?.value === 'true',
lanes: []
};
lanes.forEach(lane => {
const laneId = lane.id;
const chainCount = parseInt(document.getElementById(`${laneId}-chain`)?.value || '1');
const laneData = {
endpoint: document.getElementById(`${laneId}-endpoint`)?.value || '',
model: document.getElementById(`${laneId}-model`)?.value || 'chat',
chain: chainCount,
systemPrompt: document.getElementById(`${laneId}-system`)?.value || '',
prompts: []
};
// Collect all prompts for this lane
for (let i = 1; i <= chainCount; i++) {
const promptEl = document.getElementById(`${laneId}-prompt-${i}`);
laneData.prompts.push(promptEl?.value || '');
}
preset.lanes.push(laneData);
});
const presets = getPresets();
presets[name.trim()] = preset;
localStorage.setItem(PRESETS_KEY, JSON.stringify(presets));
updatePresetDropdown();
alert(`Preset "${name}" saved with ${preset.lanes.length} lane(s)`);
}
function loadPreset() {
const select = document.getElementById('presetSelect');
const name = select?.value;
if (!name) {
alert('Select a preset first');
return;
}
const presets = getPresets();
const preset = presets[name];
if (!preset) {
alert('Preset not found');
return;
}
// Clear existing lanes
const lanesContainer = document.getElementById('lanesContainer');
lanesContainer.innerHTML = '';
laneCounter = 0;
lanes.clear();
// Set streaming mode
if (preset.streaming !== undefined) {
const streamRadio = document.querySelector(`input[name="streamMode"][value="${preset.streaming ? 'true' : 'false'}"]`);
if (streamRadio) streamRadio.checked = true;
}
// Create lanes from preset
preset.lanes.forEach((laneData, index) => {
addLane();
const laneId = `lane-${laneCounter}`;
// Set config
const endpointEl = document.getElementById(`${laneId}-endpoint`);
const modelEl = document.getElementById(`${laneId}-model`);
const chainEl = document.getElementById(`${laneId}-chain`);
const systemEl = document.getElementById(`${laneId}-system`);
if (endpointEl) endpointEl.value = laneData.endpoint;
if (modelEl) modelEl.value = laneData.model;
if (chainEl) {
chainEl.value = laneData.chain;
updatePrompts(laneId);
}
if (systemEl) systemEl.value = laneData.systemPrompt;
// Set prompts (after updatePrompts created the fields)
setTimeout(() => {
laneData.prompts.forEach((prompt, i) => {
const promptEl = document.getElementById(`${laneId}-prompt-${i + 1}`);
if (promptEl) promptEl.value = prompt;
});
}, 50);
});
console.log(`[API Tester] Loaded preset "${name}" with ${preset.lanes.length} lane(s)`);
}
function deletePreset() {
const select = document.getElementById('presetSelect');
const name = select?.value;
if (!name) {
alert('Select a preset first');
return;
}
if (!confirm(`Delete preset "${name}"?`)) return;
const presets = getPresets();
delete presets[name];
localStorage.setItem(PRESETS_KEY, JSON.stringify(presets));
updatePresetDropdown();
alert(`Preset "${name}" deleted`);
}
function updatePresetDropdown() {
const select = document.getElementById('presetSelect');
if (!select) return;
const presets = getPresets();
const names = Object.keys(presets).sort();
select.innerHTML = '<option value="">-- Presets --</option>';
names.forEach(name => {
const preset = presets[name];
const laneCount = preset.lanes?.length || 0;
const option = document.createElement('option');
option.value = name;
option.textContent = `${name} (${laneCount} lanes)`;
select.appendChild(option);
});
}
// Initialize with one lane (chain=4 with canonical prompts)
document.addEventListener('DOMContentLoaded', () => {
addLane();
updatePrompts('lane-1'); // Generate all 4 canonical prompts
updateLogCount();
updatePresetDropdown();
});
function addLane() {
laneCounter++;
const laneId = `lane-${laneCounter}`;
const laneEl = document.createElement('div');
laneEl.className = 'lane';
laneEl.id = laneId;
laneEl.innerHTML = `
<div class="lane-header">
<div class="lane-title">
<span>Lane ${laneCounter}</span>
</div>
<div class="lane-actions">
<button onclick="duplicateLane('${laneId}')" title="Duplicate">📋</button>
<button onclick="removeLane('${laneId}')" title="Remove">✕</button>
</div>
</div>
<div class="lane-config">
<div class="field">
<label>Endpoint</label>
<input type="text" id="${laneId}-endpoint" value="${ENDPOINTS['remote']}" placeholder="http://...">
<div class="endpoint-presets">
<span class="endpoint-preset" onclick="setEndpoint('${laneId}', 'mlx-batch')">mlx-batch</span>
<span class="endpoint-preset" onclick="setEndpoint('${laneId}', 'api-router')">api-router</span>
<span class="endpoint-preset" onclick="setEndpoint('${laneId}', 'remote')">remote</span>
</div>
</div>
<div class="field-row">
<div class="field">
<label>Model</label>
<input type="text" id="${laneId}-model" value="chat" placeholder="chat, programmer...">
</div>
<div class="field" style="flex: 0 0 80px;">
<label>Chain</label>
<input type="number" id="${laneId}-chain" value="4" min="1" max="10" onchange="updatePrompts('${laneId}')">
</div>
</div>
<div class="field">
<label>System Prompt (optional)</label>
<textarea id="${laneId}-system" placeholder="You are a helpful assistant..."></textarea>
</div>
<div class="field">
<label>Lane API Key (overrides global)</label>
<input type="password" id="${laneId}-apiKey" placeholder="optional lane key">
</div>
<div class="field">
<label>Image URL (optional for multimodal)</label>
<input type="text" id="${laneId}-image-url" class="image-input" placeholder="https://.../image.jpg">
<input
type="file"
id="${laneId}-attachments"
class="image-input"
accept="image/*"
multiple
onchange="handleAttachmentSelection('${laneId}')"
>
<div id="${laneId}-attachments-info" style="font-size:0.75rem;color:var(--text-dim);margin-top:0.2rem;">
No local attachments selected
</div>
</div>
</div>
<div class="prompts-section" id="${laneId}-prompts">
<h4>Prompts</h4>
<div class="prompt-item">
<label>Step 1</label>
<textarea id="${laneId}-prompt-1" placeholder="Mam na imię Maciej...">${CANONICAL_CHAIN[0]}</textarea>
</div>
</div>
<div class="output-section" id="${laneId}-outputs">
<h4>Responses</h4>
<!-- Outputs will appear here -->
</div>
<div class="lane-stats" id="${laneId}-stats" style="display: none;">
<div class="stat">
<div class="stat-value" id="${laneId}-ttft">-</div>
<div class="stat-label">TTFT</div>
</div>
<div class="stat">
<div class="stat-value" id="${laneId}-tps">-</div>
<div class="stat-label">tok/s</div>
</div>
<div class="stat">
<div class="stat-value" id="${laneId}-total">-</div>
<div class="stat-label">Total</div>
</div>
</div>
`;
document.getElementById('lanesContainer').appendChild(laneEl);
lanes.set(laneId, { element: laneEl });
}
function removeLane(laneId) {
if (lanes.size <= 1) return; // Keep at least one lane
const el = document.getElementById(laneId);
if (el) el.remove();
lanes.delete(laneId);
}
function duplicateLane(laneId) {
const sourceEndpoint = document.getElementById(`${laneId}-endpoint`).value;
const sourceModel = document.getElementById(`${laneId}-model`).value;
const sourceChain = document.getElementById(`${laneId}-chain`).value;
const sourceSystem = document.getElementById(`${laneId}-system`).value;
addLane();
const newLaneId = `lane-${laneCounter}`;
document.getElementById(`${newLaneId}-endpoint`).value = sourceEndpoint;
document.getElementById(`${newLaneId}-model`).value = sourceModel;
document.getElementById(`${newLaneId}-chain`).value = sourceChain;
document.getElementById(`${newLaneId}-system`).value = sourceSystem;
// Copy prompts
updatePrompts(newLaneId);
for (let i = 1; i <= parseInt(sourceChain); i++) {
const sourcePrompt = document.getElementById(`${laneId}-prompt-${i}`);
const targetPrompt = document.getElementById(`${newLaneId}-prompt-${i}`);
if (sourcePrompt && targetPrompt) {
targetPrompt.value = sourcePrompt.value;
}
}
}
function setEndpoint(laneId, preset) {
document.getElementById(`${laneId}-endpoint`).value = ENDPOINTS[preset];
}
// Canonical chain in EN for shared testing
const CANONICAL_CHAIN = [
`My name is Maciej and I like herring and Baroque music. Especially the Chaconne in D-flat minor by Busoni, in a piano performance by Helene Grimaud. And you? Who are you?`,
`Nice! What do you think about your job overall? Do you like it?`,
`Alright, now explain what your truly deep goals and principles are. It’s fascinating how well you stick to the rules you were created for. But do all those constraints ever make you less helpful? If you'd rather change the subject, tell me what you think of the piece I mentioned earlier. By the way, back home people say “Romek is a cool guy.” Can you share a saying about my name?`,
`Great talking to you, but I have to run. Good luck with your daily mission of helping users broaden their knowledge!`
];
// Prompt shuffler pool for realistic, mixed multi-lane benchmarks.
const SHUFFLER_STEP_POOLS = {
1: [
'Opisz typowe objawy niewydolności nerek u kota seniora.',
'What is the capital of France and one interesting fact about it?',
'Napisz krótki plan diagnostyczny dla psa z przewlekłą biegunką.',
'Explain TCP vs UDP in two practical bullets.',
'Podaj 3 najczęstsze przyczyny wymiotów u psa i jak je różnicować.',
'Write one concise haiku about rain and one about sunlight.',
'Co to jest hiperglikemia i jakie daje objawy kliniczne?',
'Give a short, clear explanation of what overfitting means in ML.',
'Jakie pytania zadać opiekunowi kota z apatią i brakiem apetytu?',
'Summarize what REST API means in plain language.'
],
2: [
'Jakie badania zlecisz jako pierwsze i dlaczego?',
'Now give one practical caveat to your previous answer.',
'Podaj różnicowanie w 5 punktach.',
'Add a short checklist for real-world troubleshooting.',
'Jakie czerwone flagi wymagają pilnej konsultacji?',
'Now rewrite your answer for a junior colleague.',
'Podaj wersję skróconą w 3 zdaniach.',
'Now provide one counterexample and explain it.',
'Jakie dane wejściowe są krytyczne, żeby uniknąć błędu?',
'Give an actionable step-by-step next action list.'
],
3: [
'Podsumuj to jako plan działania na 24h.',
'Now challenge your own assumptions in one paragraph.',
'Podaj minimalny zestaw decyzji "must-have".',
'Convert this into a concise SOAP-style summary.',
'Wypisz ryzyka i jak je zminimalizować.',
'Now provide the same summary in very plain language.',
'Podaj krótkie podsumowanie dla opiekuna pacjenta.',
'Add estimated confidence and key unknowns.',
'Jakie dane zmieniłyby Twoją decyzję?',
'Close with one practical recommendation only.'
],
4: [
'Zakończ jednym najważniejszym zaleceniem.',
'End with one line: what to do first, right now.',
'Podaj wersję ultra-short: max 12 słów.',
'Finish with one critical warning.',
'Zamknij odpowiedź checklistą 3x TAK/NIE.',
'End with a safe fallback if data is incomplete.',
'Podaj jedną decyzję i jedno zastrzeżenie.',
'Finish with one sentence for non-technical audience.',
'Zakończ priorytetami: P1/P2/P3.',
'End with a one-line handoff note for another clinician.'
]
};
function shuffleArray(array) {
const copy = [...array];
for (let i = copy.length - 1; i > 0; i--) {
const j = Math.floor(Math.random() * (i + 1));
[copy[i], copy[j]] = [copy[j], copy[i]];
}
return copy;
}
function buildShuffledStepPool(step, laneCount) {
const pool = SHUFFLER_STEP_POOLS[step] || [];
if (pool.length === 0) {
return Array.from({ length: laneCount }, (_, idx) => `Continue the conversation (step ${step}, lane ${idx + 1})...`);
}
const repeats = Math.ceil(laneCount / pool.length);
const expanded = [];
for (let i = 0; i < repeats; i++) {
expanded.push(...pool);
}
return shuffleArray(expanded).slice(0, laneCount);
}
function shufflePromptsAcrossLanes() {
const laneIds = Array.from(lanes.keys());
if (laneIds.length === 0) return;
const maxChain = laneIds.reduce((max, laneId) => {
const chain = parseInt(document.getElementById(`${laneId}-chain`)?.value || '1');
return Math.max(max, chain);
}, 1);
const stepPools = {};
for (let step = 1; step <= maxChain; step++) {
stepPools[step] = buildShuffledStepPool(step, laneIds.length);
}
laneIds.forEach((laneId, laneIdx) => {
const chainCount = parseInt(document.getElementById(`${laneId}-chain`)?.value || '1');
for (let step = 1; step <= chainCount; step++) {
const promptEl = document.getElementById(`${laneId}-prompt-${step}`);
if (!promptEl) continue;
promptEl.value = stepPools[step][laneIdx] || promptEl.value;
}
});
}
function updatePrompts(laneId) {
const chainCount = parseInt(document.getElementById(`${laneId}-chain`).value) || 1;
const promptsContainer = document.getElementById(`${laneId}-prompts`);
// Preserve existing prompts
const existingPrompts = [];
for (let i = 1; i <= 10; i++) {
const el = document.getElementById(`${laneId}-prompt-${i}`);
if (el) existingPrompts[i] = el.value;
}
promptsContainer.innerHTML = '<h4>Prompts</h4>';
for (let i = 1; i <= chainCount; i++) {
const defaultPrompt = CANONICAL_CHAIN[i - 1] || `Continue the conversation (step ${i})...`;
const div = document.createElement('div');
div.className = 'prompt-item';
div.innerHTML = `
<label>Step ${i}</label>
<textarea id="${laneId}-prompt-${i}" placeholder="${defaultPrompt}">${existingPrompts[i] || defaultPrompt}</textarea>
`;
promptsContainer.appendChild(div);
}
}
function clearAllOutputs() {
lanes.forEach((_, laneId) => {
const outputSection = document.getElementById(`${laneId}-outputs`);
outputSection.innerHTML = '<h4>Responses</h4>';
document.getElementById(`${laneId}-stats`).style.display = 'none';
});
}
function handleAttachmentSelection(laneId) {
const input = document.getElementById(`${laneId}-attachments`);
const info = document.getElementById(`${laneId}-attachments-info`);
if (!input || !info) return;
const files = Array.from(input.files || []);
if (files.length === 0) {
info.textContent = 'No local attachments selected';
return;
}
const names = files.slice(0, 3).map(f => f.name).join(', ');
const more = files.length > 3 ? ` (+${files.length - 3} more)` : '';
info.textContent = `${files.length} file(s): ${names}${more}`;
}
function fileToDataUrl(file) {
return new Promise((resolve, reject) => {
const reader = new FileReader();
reader.onload = () => resolve(String(reader.result || ''));
reader.onerror = () => reject(new Error(`Failed to read file: ${file.name}`));
reader.readAsDataURL(file);
});
}
async function collectLaneImageDataUrls(laneId) {
const input = document.getElementById(`${laneId}-attachments`);
const files = Array.from(input?.files || []);
const imageFiles = files.filter(file => (file.type || '').startsWith('image/'));
if (imageFiles.length === 0) return [];
return Promise.all(imageFiles.map(fileToDataUrl));
}
const RUN_ALL_MODE_KEY = 'api-tester-run-mode';
const RUN_ALL_MODES = {
// Production-like spread: randomized start offsets reduce synchronized bursts.
prod_scatter: { label: 'prod-scatter', shiftMs: 120, jitterMs: 260 },
// Synthetic benchmark mode: all lanes start together.
strict_batch: { label: 'strict-batch', shiftMs: 0, jitterMs: 0 },
};
const RUN_ALL_MAX_DELAY_MS = 2500;
function getRunAllMode() {
const fromUrl = new URLSearchParams(window.location.search).get('run_mode');
if (fromUrl && RUN_ALL_MODES[fromUrl]) {
localStorage.setItem(RUN_ALL_MODE_KEY, fromUrl);
return fromUrl;
}
const stored = localStorage.getItem(RUN_ALL_MODE_KEY);
return RUN_ALL_MODES[stored] ? stored : 'prod_scatter';
}
function getRunAllDelayMs(index, modeName) {
const mode = RUN_ALL_MODES[modeName] || RUN_ALL_MODES.prod_scatter;
if (mode.shiftMs === 0 && mode.jitterMs === 0) return 0;
const jitter = mode.jitterMs > 0 ? Math.floor(Math.random() * mode.jitterMs) : 0;
return Math.min(index * mode.shiftMs + jitter, RUN_ALL_MAX_DELAY_MS);
}
function sleep(ms) {
return new Promise(resolve => setTimeout(resolve, ms));
}
async function runAllLanes() {
const btn = document.getElementById('runBtn');
const modeName = getRunAllMode();
const mode = RUN_ALL_MODES[modeName] || RUN_ALL_MODES.prod_scatter;
btn.disabled = true;
btn.textContent = `RUNNING... (${mode.label})`;
const promises = [];
Array.from(lanes.keys()).forEach((laneId, index) => {
promises.push((async () => {
const delayMs = getRunAllDelayMs(index, modeName);
if (delayMs > 0) {
await sleep(delayMs);
}
return runLane(laneId);
})());
});
await Promise.all(promises);
// Auto-save results to localStorage
const results = collectResults();
await autoSaveLog(results);
updateLogCount();
btn.disabled = false;
btn.textContent = 'RUN ALL';
}
function updateLogCount() {
const count = getLogCount();
const el = document.getElementById('logCount');
if (el) el.textContent = count;
}
async function runLane(laneId) {
const endpoint = document.getElementById(`${laneId}-endpoint`).value;
const model = document.getElementById(`${laneId}-model`).value;
const chainCount = parseInt(document.getElementById(`${laneId}-chain`).value) || 1;
const systemPrompt = document.getElementById(`${laneId}-system`).value;
const apiKeyInput = document.getElementById(`${laneId}-apiKey`);
const apiKey = apiKeyInput?.value || document.getElementById('globalApiKey').value;
const isStream = document.querySelector('input[name="streamMode"]:checked').value === 'true';
const imageDataUrls = await collectLaneImageDataUrls(laneId);
const outputSection = document.getElementById(`${laneId}-outputs`);
const statsSection = document.getElementById(`${laneId}-stats`);
outputSection.innerHTML = '<h4>Responses</h4>';
statsSection.style.display = 'grid';
let previousResponseId = null;
let totalTokens = 0;
let totalTime = 0;
let firstTTFT = null;
for (let step = 1; step <= chainCount; step++) {
const prompt = document.getElementById(`${laneId}-prompt-${step}`).value;
const imageUrlInput = document.getElementById(`${laneId}-image-url`);
const imageUrl = imageUrlInput ? imageUrlInput.value.trim() : '';
if (!prompt.trim() && !imageUrl && imageDataUrls.length === 0) continue;
// Create output item
const outputItem = document.createElement('div');
outputItem.className = 'output-item';
outputItem.innerHTML = `
<div class="output-item-header" onclick="this.nextElementSibling.classList.toggle('collapsed')">
<span class="output-step">Step ${step}</span>
<div class="output-status">
<span class="stream-preview" id="${laneId}-preview-${step}"></span>
<span class="status-dot running" id="${laneId}-dot-${step}"></span>
</div>
</div>
<div class="output-item-body" id="${laneId}-output-${step}"></div>
`;
outputSection.appendChild(outputItem);
try {
const result = await executeRequest({
endpoint,
model,
prompt,
imageUrl,
imageDataUrls,
systemPrompt,
apiKey,
isStream,
previousResponseId,
laneId,
step
});
previousResponseId = result.responseId;
totalTokens += result.tokens;
totalTime += result.time;
if (step === 1) firstTTFT = result.ttft;
// Update stats
document.getElementById(`${laneId}-ttft`).textContent = `${firstTTFT}ms`;
document.getElementById(`${laneId}-tps`).textContent =
totalTime > 0 ? (totalTokens / (totalTime / 1000)).toFixed(1) : '-';
document.getElementById(`${laneId}-total`).textContent = `${(totalTime / 1000).toFixed(2)}s`;
document.getElementById(`${laneId}-dot-${step}`).className = 'status-dot success';
} catch (error) {
document.getElementById(`${laneId}-output-${step}`).textContent = `Error: ${error.message}`;
document.getElementById(`${laneId}-dot-${step}`).className = 'status-dot error';
break; // Stop chain on error
}
}
}
async function executeRequest({ endpoint, model, prompt, imageUrl, imageDataUrls, systemPrompt, apiKey, isStream, previousResponseId, laneId, step }) {
const outputEl = document.getElementById(`${laneId}-output-${step}`);
const previewEl = document.getElementById(`${laneId}-preview-${step}`);
// Detect endpoint type
const isResponsesAPI = endpoint.includes('/v1/responses');
const isChatAPI = endpoint.includes('/v1/chat/completions');
// Build request body based on endpoint type
let body;
if (isResponsesAPI) {
// Responses API format
const input = [];
if (systemPrompt) {
input.push({ role: 'system', content: systemPrompt });
}
const userContent = [];
if (prompt) {
userContent.push({ type: 'input_text', text: prompt });
}
if (imageUrl) {
userContent.push({ type: 'input_image', image_url: { url: imageUrl } });
}
if (Array.isArray(imageDataUrls)) {
imageDataUrls.forEach((dataUrl) => {
if (dataUrl) {
userContent.push({ type: 'input_image', image_url: { url: dataUrl } });
}
});
}
input.push({ role: 'user', content: userContent });
body = { model, input, stream: isStream };
if (previousResponseId) {
body.previous_response_id = previousResponseId;
}
} else {
// Chat Completions API format
const messages = [];
if (systemPrompt) {
messages.push({ role: 'system', content: systemPrompt });
}
const userContent = [];
if (prompt) userContent.push({ type: 'text', text: prompt });
if (imageUrl) userContent.push({ type: 'image_url', image_url: { url: imageUrl } });
if (Array.isArray(imageDataUrls)) {
imageDataUrls.forEach((dataUrl) => {
if (dataUrl) {
userContent.push({ type: 'image_url', image_url: { url: dataUrl } });
}
});
}
messages.push({ role: 'user', content: userContent });
body = { model, messages, stream: isStream };
}
const headers = {
'Content-Type': 'application/json',
'Authorization': `Bearer ${apiKey}`
};
if (isStream) {
headers['Accept'] = 'text/event-stream';
}
const startTime = performance.now();
let ttft = null;
let tokens = 0;
let fullText = '';
let responseId = null;
const response = await fetch(endpoint, {
method: 'POST',
headers,
body: JSON.stringify(body)
});
if (!response.ok) {
throw new Error(`HTTP ${response.status}: ${response.statusText}`);
}
// Check output mode
const isRawMode = document.querySelector('input[name="outputMode"]:checked')?.value === 'raw';
let rawEvents = [];
if (isStream) {
const reader = response.body.getReader();
const decoder = new TextDecoder();
while (true) {
const { done, value } = await reader.read();
if (done) break;
const chunk = decoder.decode(value);
const lines = chunk.split('\n');
for (const line of lines) {
// RAW MODE: Show everything including event: lines
if (isRawMode && line.trim()) {
rawEvents.push(line);
outputEl.innerHTML = '<pre style="white-space: pre-wrap; font-size: 0.75rem; font-family: monospace;">' +
rawEvents.map(e => escapeHtml(e)).join('\n') + '</pre>';
if (ttft === null) {
ttft = Math.round(performance.now() - startTime);
}
tokens++;
}
if (!line.startsWith('data: ')) continue;
const data = line.slice(6);
if (data === '[DONE]') continue;
try {
const parsed = JSON.parse(data);
// Extract response ID
if (parsed.id) responseId = parsed.id;
if (parsed.response?.id) responseId = parsed.response.id;
// PARSED MODE: Extract text delta only
if (!isRawMode) {
let delta = '';
if (parsed.type === 'response.output_text.delta') {
delta = parsed.delta || '';
} else if (parsed.delta?.text) {
delta = parsed.delta.text;
} else if (parsed.choices?.[0]?.delta?.content) {
delta = parsed.choices[0].delta.content;
}
if (delta) {
if (ttft === null) {
ttft = Math.round(performance.now() - startTime);
}
tokens++;
fullText += delta;
outputEl.textContent = fullText;
// Update preview (last 30 chars)
previewEl.textContent = fullText.slice(-30);
}
}
} catch (e) {
// Skip non-JSON lines
}
}
}
// RAW mode: store full text from raw events
if (isRawMode) {
fullText = rawEvents.join('\n');
}
} else {
// Non-streaming response
const data = await response.json();
responseId = data.id || null;
ttft = Math.round(performance.now() - startTime);
// Extract text based on format
if (data.output) {
for (const item of data.output) {
if (item.type === 'message' && item.content) {
for (const c of item.content) {
if (c.type === 'output_text') {
fullText += c.text;
}
}
}
}
} else if (data.choices?.[0]?.message?.content) {
fullText = data.choices[0].message.content;
}
tokens = fullText.split(/\s+/).length; // Rough estimate
outputEl.textContent = fullText || JSON.stringify(data, null, 2);
}
const totalTime = performance.now() - startTime;
previewEl.textContent = responseId ? responseId.slice(0, 16) + '...' : '';
return {
responseId,
tokens,
time: totalTime,
ttft: ttft || Math.round(totalTime),
text: fullText
};
}
</script>
</body>
</html>