Skip to content

Commit ad1bfb0

Browse files
committed
feat(viewer): show request message counts
1 parent ffd6d5d commit ad1bfb0

File tree

4 files changed

+175
-3
lines changed

4 files changed

+175
-3
lines changed

src/controllers/viewer-controller.js

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,7 @@ import {
99
loadViewerLog,
1010
parseCompareLogSelection,
1111
} from '../services/viewer-service.js';
12-
import { buildPreviewModel } from '../services/viewer-preview.js';
12+
import { buildPreviewModel, getRequestMessageCount } from '../services/viewer-preview.js';
1313
import {
1414
normalizeBaseUrlFilters,
1515
normalizeBaseUrlValue,
@@ -44,6 +44,7 @@ export function createViewerController(config) {
4444
(providerKey && aliasNameMap[providerKey]) ||
4545
(baseUrl ? aliasByHost[baseUrl] : null) ||
4646
null;
47+
const messageCount = getRequestMessageCount(log);
4748
try {
4849
const url = new URL(log.request.url);
4950
const hidden = shouldHideFromViewer(url.pathname);
@@ -53,13 +54,15 @@ export function createViewerController(config) {
5354
_path: url.pathname,
5455
_base_url: baseUrl,
5556
_alias: aliasLabel,
57+
_message_count: messageCount,
5658
};
5759
} catch {
5860
return {
5961
...log,
6062
_hidden: false,
6163
_base_url: baseUrl,
6264
_alias: aliasLabel,
65+
_message_count: messageCount,
6366
};
6467
}
6568
});

src/services/viewer-preview.js

Lines changed: 98 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -40,7 +40,7 @@ const FALLBACK_SHAPE = {
4040
export function buildPreviewModel(log) {
4141
const errors = [];
4242
const urlInfo = parseUrlInfo(log?.request?.url);
43-
const shape = SHAPES.find((candidate) => candidate.match(urlInfo)) || FALLBACK_SHAPE;
43+
const shape = resolveShape(urlInfo);
4444

4545
let normalized;
4646
try {
@@ -65,6 +65,20 @@ export function buildPreviewModel(log) {
6565
};
6666
}
6767

68+
export function getRequestMessageCount(log) {
69+
const urlInfo = parseUrlInfo(log?.request?.url);
70+
const shape = resolveShape(urlInfo);
71+
if (shape.id === 'other') return null;
72+
73+
try {
74+
const requestMessages = normalizeRequestMessages(shape.id, log);
75+
if (!Array.isArray(requestMessages)) return null;
76+
return requestMessages.length;
77+
} catch {
78+
return null;
79+
}
80+
}
81+
6882
function parseUrlInfo(rawUrl) {
6983
if (!rawUrl || typeof rawUrl !== 'string') {
7084
return { raw: '', pathname: '', hostname: '' };
@@ -82,6 +96,10 @@ function parseUrlInfo(rawUrl) {
8296
}
8397
}
8498

99+
function resolveShape(urlInfo) {
100+
return SHAPES.find((candidate) => candidate.match(urlInfo)) || FALLBACK_SHAPE;
101+
}
102+
85103
function normalizeCompletions(log, urlInfo, errors) {
86104
const requestBody = asObject(log?.request?.body);
87105
const responseBody = log?.response?.body;
@@ -317,6 +335,85 @@ function normalizeOther(log) {
317335
};
318336
}
319337

338+
function normalizeRequestMessages(shapeId, log) {
339+
const requestBody = asObject(log?.request?.body);
340+
341+
if (shapeId === 'completions') {
342+
return normalizeCompletionsRequestMessages(requestBody);
343+
}
344+
if (shapeId === 'responses') {
345+
return normalizeResponsesInput(requestBody);
346+
}
347+
if (shapeId === 'anthropic') {
348+
return normalizeAnthropicRequestMessages(requestBody);
349+
}
350+
if (shapeId === 'gemini') {
351+
return normalizeGeminiRequestMessages(requestBody);
352+
}
353+
354+
return null;
355+
}
356+
357+
function normalizeCompletionsRequestMessages(requestBody) {
358+
const requestMessages = normalizeOpenAIChatMessages(requestBody.messages);
359+
if (requestMessages.length === 0 && typeof requestBody.prompt === 'string') {
360+
requestMessages.push({
361+
role: 'user',
362+
name: null,
363+
parts: [{ type: 'text', text: requestBody.prompt }],
364+
metadata: {},
365+
});
366+
} else if (requestMessages.length === 0 && Array.isArray(requestBody.prompt)) {
367+
for (const prompt of requestBody.prompt) {
368+
if (typeof prompt === 'string') {
369+
requestMessages.push({
370+
role: 'user',
371+
name: null,
372+
parts: [{ type: 'text', text: prompt }],
373+
metadata: {},
374+
});
375+
}
376+
}
377+
}
378+
return requestMessages;
379+
}
380+
381+
function normalizeAnthropicRequestMessages(requestBody) {
382+
const requestMessages = [];
383+
if (typeof requestBody.system === 'string') {
384+
requestMessages.push({
385+
role: 'system',
386+
name: null,
387+
parts: [{ type: 'text', text: requestBody.system }],
388+
metadata: {},
389+
});
390+
} else if (Array.isArray(requestBody.system)) {
391+
requestMessages.push({
392+
role: 'system',
393+
name: null,
394+
parts: normalizeAnthropicContent(requestBody.system),
395+
metadata: {},
396+
});
397+
}
398+
requestMessages.push(...normalizeAnthropicMessages(requestBody.messages));
399+
return requestMessages;
400+
}
401+
402+
function normalizeGeminiRequestMessages(requestBody) {
403+
const requestMessages = [];
404+
if (requestBody.systemInstruction) {
405+
const parts = normalizeGeminiParts(requestBody.systemInstruction.parts || requestBody.systemInstruction);
406+
requestMessages.push({
407+
role: 'system',
408+
name: null,
409+
parts,
410+
metadata: {},
411+
});
412+
}
413+
requestMessages.push(...normalizeGeminiMessages(requestBody.contents));
414+
return requestMessages;
415+
}
416+
320417
function normalizeOpenAIChatMessages(messages) {
321418
if (!Array.isArray(messages)) return [];
322419
return messages

src/templates/viewer.ejs

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -917,6 +917,7 @@
917917
<% const baseUrlFilterValue = baseUrlLabel; %>
918918
<% const providerLabel = providerName; %>
919919
<% const appLabel = appName; %>
920+
<% const messageCount = Number.isFinite(log._message_count) ? log._message_count : null; %>
920921
<% const normalizedBaseUrlDisplay = (baseUrlDisplay || '').toLowerCase(); %>
921922
<% const normalizedAlias = (aliasLabel || '').toLowerCase(); %>
922923
<% const normalizedProvider = (providerLabel || '').toLowerCase(); %>
@@ -950,6 +951,7 @@
950951
<span class="pill status <%= log.response.status < 400 ? 'ok' : 'bad' %>"><%= log.response.status %></span>
951952
<span class="pill"><%= log.duration_ms || 0 %>ms</span>
952953
<span class="pill"><%= new Date(log.timestamp).toLocaleTimeString() %></span>
954+
<span class="pill"><%= messageCount !== null ? `Messages: ${messageCount}` : 'Messages: n/a' %></span>
953955
<% if (baseUrlDisplay) { %>
954956
<a class="pill pill-link" href="<%= buildBaseUrlLink(baseUrlFilterValue) %>"><%= baseUrlDisplay %></a>
955957
<% } %>

tests/viewer-preview.test.js

Lines changed: 71 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
import { describe, it } from 'node:test';
22
import assert from 'node:assert';
3-
import { buildPreviewModel } from '../src/services/viewer-preview.js';
3+
import { buildPreviewModel, getRequestMessageCount } from '../src/services/viewer-preview.js';
44

55
describe('viewer preview', () => {
66
it('detects completions shape by URL', () => {
@@ -114,4 +114,74 @@ describe('viewer preview', () => {
114114
assert.deepStrictEqual(preview.request.messages, []);
115115
assert.deepStrictEqual(preview.response.messages, []);
116116
});
117+
118+
it('counts request messages for completions prompts', () => {
119+
const log = {
120+
request: {
121+
url: 'https://api.openai.com/v1/chat/completions',
122+
body: {
123+
prompt: ['Hello', 'World'],
124+
},
125+
},
126+
response: {},
127+
};
128+
129+
assert.strictEqual(getRequestMessageCount(log), 2);
130+
});
131+
132+
it('counts request messages for responses inputs', () => {
133+
const log = {
134+
request: {
135+
url: 'https://api.openai.com/v1/responses',
136+
body: {
137+
input: 'Summarize this.',
138+
},
139+
},
140+
response: {},
141+
};
142+
143+
assert.strictEqual(getRequestMessageCount(log), 1);
144+
});
145+
146+
it('counts request messages for anthropic system plus messages', () => {
147+
const log = {
148+
request: {
149+
url: 'https://api.anthropic.com/v1/messages',
150+
body: {
151+
system: 'You are Claude.',
152+
messages: [{ role: 'user', content: 'Hey' }],
153+
},
154+
},
155+
response: {},
156+
};
157+
158+
assert.strictEqual(getRequestMessageCount(log), 2);
159+
});
160+
161+
it('counts request messages for gemini system and contents', () => {
162+
const log = {
163+
request: {
164+
url: 'https://generativelanguage.googleapis.com/v1beta/models/gemini-pro:generateContent',
165+
body: {
166+
systemInstruction: { parts: [{ text: 'Be concise.' }] },
167+
contents: [{ role: 'user', parts: [{ text: 'Hi' }] }],
168+
},
169+
},
170+
response: {},
171+
};
172+
173+
assert.strictEqual(getRequestMessageCount(log), 2);
174+
});
175+
176+
it('returns null for unknown request shapes', () => {
177+
const log = {
178+
request: {
179+
url: 'https://example.com/v1/other',
180+
body: { foo: 'bar' },
181+
},
182+
response: {},
183+
};
184+
185+
assert.strictEqual(getRequestMessageCount(log), null);
186+
});
117187
});

0 commit comments

Comments
 (0)