omniharness-cli 0.1.83 → 0.1.84
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/dist/config/omniRoute.js
CHANGED
|
@@ -32,6 +32,7 @@ export class OmniRouteClient {
|
|
|
32
32
|
metrics = {
|
|
33
33
|
compression: { inputTokens: 0, compressedTokens: 0, ratio: 1, strategy: 'none', updatedAt: new Date().toISOString() },
|
|
34
34
|
fallback: { attempts: 0 },
|
|
35
|
+
usage: { contextTokens: 0, tokensIn: 0, tokensOut: 0, costUsd: 0 },
|
|
35
36
|
requestCount: 0,
|
|
36
37
|
};
|
|
37
38
|
constructor(config = {}) {
|
|
@@ -186,7 +187,19 @@ export class OmniRouteClient {
|
|
|
186
187
|
let lineBuffer = '';
|
|
187
188
|
// Partially-accumulated tool calls keyed by stream index.
|
|
188
189
|
const toolStreams = new Map();
|
|
190
|
+
// OmniRoute's response headers are sent at stream start, before the
|
|
191
|
+
// latency, usage and cost are known, so on a stream they carry zeros. With
|
|
192
|
+
// OMNIROUTE_SSE_COMMENTS on, the gateway ends the stream with the same
|
|
193
|
+
// fields as `: x-omniroute-<name>=<value>` comment lines — the values that
|
|
194
|
+
// were final. They are collected here and read like a second header set.
|
|
195
|
+
const trailer = new Headers();
|
|
189
196
|
const flushData = (line) => {
|
|
197
|
+
if (line.startsWith(':')) {
|
|
198
|
+
const meta = /^:\s*(x-omniroute-[a-z0-9-]+)=(.*)$/i.exec(line);
|
|
199
|
+
if (meta)
|
|
200
|
+
trailer.set(meta[1].toLowerCase(), meta[2].trim());
|
|
201
|
+
return;
|
|
202
|
+
}
|
|
190
203
|
const sep = line.indexOf(':');
|
|
191
204
|
if (sep === -1)
|
|
192
205
|
return;
|
|
@@ -274,6 +287,10 @@ export class OmniRouteClient {
|
|
|
274
287
|
const toolCalls = [...toolStreams.values()]
|
|
275
288
|
.filter((entry) => entry.id !== '' && entry.name !== '')
|
|
276
289
|
.map((entry) => ({ id: entry.id, type: 'function', function: { name: entry.name, arguments: entry.argsFragments.join('') } }));
|
|
290
|
+
// The trailer names the provider and model that finished the stream, so it
|
|
291
|
+
// supersedes the routing picture taken from the initial headers.
|
|
292
|
+
this.updateMetrics(trailer);
|
|
293
|
+
this.recordCompletion(response.headers, usage, trailer);
|
|
277
294
|
return { content, model: answered, finishReason, reasoning: reasoning || undefined, toolCalls, usage, headers: response.headers, compression: this.compressionFrom(response.headers) };
|
|
278
295
|
}
|
|
279
296
|
async chat(model, messages, options = {}) {
|
|
@@ -294,6 +311,11 @@ export class OmniRouteClient {
|
|
|
294
311
|
// (the combo may have routed anywhere); fall back to the requested id.
|
|
295
312
|
const answered = typeof payload.model === 'string' && payload.model.trim() !== '' ? payload.model : model;
|
|
296
313
|
const toolCalls = Array.isArray(message.tool_calls) ? message.tool_calls.map((call) => this.asToolCall(call)).filter((call) => call !== null) : undefined;
|
|
314
|
+
this.recordCompletion(response.headers, usage ? {
|
|
315
|
+
inputTokens: this.number(usage.prompt_tokens),
|
|
316
|
+
outputTokens: this.number(usage.completion_tokens),
|
|
317
|
+
totalTokens: this.number(usage.total_tokens),
|
|
318
|
+
} : undefined);
|
|
297
319
|
return {
|
|
298
320
|
content: typeof message.content === 'string' ? message.content : '',
|
|
299
321
|
model: answered,
|
|
@@ -309,6 +331,50 @@ export class OmniRouteClient {
|
|
|
309
331
|
compression: this.compressionFrom(response.headers),
|
|
310
332
|
};
|
|
311
333
|
}
|
|
334
|
+
/**
|
|
335
|
+
* OmniRoute's cost-telemetry set for one completion, or undefined when the
|
|
336
|
+
* headers carry no token counts. A stream's initial headers hold zeros for
|
|
337
|
+
* every field the gateway could not know yet, and zeros are treated as
|
|
338
|
+
* absent so they never overwrite a count read elsewhere.
|
|
339
|
+
*/
|
|
340
|
+
usageFromHeaders(headers) {
|
|
341
|
+
const inputTokens = this.headerNumber(headers, 'x-omniroute-tokens-in') ?? 0;
|
|
342
|
+
const outputTokens = this.headerNumber(headers, 'x-omniroute-tokens-out') ?? 0;
|
|
343
|
+
if (inputTokens <= 0 && outputTokens <= 0)
|
|
344
|
+
return undefined;
|
|
345
|
+
const costUsd = this.headerNumber(headers, 'x-omniroute-response-cost');
|
|
346
|
+
const latencyMs = this.headerNumber(headers, 'x-omniroute-latency-ms');
|
|
347
|
+
return {
|
|
348
|
+
inputTokens: Math.max(0, inputTokens),
|
|
349
|
+
outputTokens: Math.max(0, outputTokens),
|
|
350
|
+
costUsd: costUsd !== undefined && costUsd > 0 ? costUsd : undefined,
|
|
351
|
+
latencyMs: latencyMs !== undefined && latencyMs > 0 ? latencyMs : undefined,
|
|
352
|
+
};
|
|
353
|
+
}
|
|
354
|
+
/**
|
|
355
|
+
* Fold one completion into the session's usage. Sources, most authoritative
|
|
356
|
+
* first: the SSE metadata trailer (final values of a stream), the response
|
|
357
|
+
* headers (final on a non-streaming reply, zeros on a stream), then the
|
|
358
|
+
* `usage` object in the body. Token counts come from one source only, so a
|
|
359
|
+
* reply that reports itself twice is still counted once.
|
|
360
|
+
*/
|
|
361
|
+
recordCompletion(headers, body, trailer) {
|
|
362
|
+
const usage = this.metrics.usage;
|
|
363
|
+
const measured = (trailer && this.usageFromHeaders(trailer))
|
|
364
|
+
?? this.usageFromHeaders(headers)
|
|
365
|
+
?? (body && (body.inputTokens > 0 || body.outputTokens > 0) ? { inputTokens: body.inputTokens, outputTokens: body.outputTokens } : undefined);
|
|
366
|
+
if (!measured)
|
|
367
|
+
return;
|
|
368
|
+
if (measured.inputTokens > 0)
|
|
369
|
+
usage.contextTokens = measured.inputTokens;
|
|
370
|
+
usage.tokensIn += measured.inputTokens;
|
|
371
|
+
usage.tokensOut += measured.outputTokens;
|
|
372
|
+
if (measured.costUsd !== undefined)
|
|
373
|
+
usage.costUsd += measured.costUsd;
|
|
374
|
+
if (measured.latencyMs !== undefined)
|
|
375
|
+
usage.latencyMs = measured.latencyMs;
|
|
376
|
+
usage.updatedAt = new Date().toISOString();
|
|
377
|
+
}
|
|
312
378
|
compressionFrom(headers) {
|
|
313
379
|
const input = this.headerNumber(headers, 'x-omniroute-input-tokens');
|
|
314
380
|
const compressed = this.headerNumber(headers, 'x-omniroute-compressed-tokens');
|
|
@@ -981,9 +981,12 @@ export function TerminalInterface({ engine }) {
|
|
|
981
981
|
const contentWidth = Math.max(20, convoWidth - 6);
|
|
982
982
|
const terminalRows = rows;
|
|
983
983
|
const metrics = engine.client.snapshotMetrics();
|
|
984
|
-
|
|
984
|
+
// The prompt tokens of the last completion, as the gateway counted them,
|
|
985
|
+
// are the size of the context the next turn will carry.
|
|
986
|
+
const contextTokens = metrics.usage?.contextTokens ?? 0;
|
|
987
|
+
const meter = contextMeter(contextTokens, engine.state.activeModel, metrics.fallback.activeProvider);
|
|
985
988
|
const meterColor = meter.zone === 'danger' ? PALETTE.error : meter.zone === 'warn' ? PALETTE.warn : PALETTE.muted;
|
|
986
|
-
const contextLabel =
|
|
989
|
+
const contextLabel = contextTokens > 0 ? `ctx ${meterBar(meter.fraction, 8)} ${Math.round(meter.fraction * 100)}%` : '';
|
|
987
990
|
const compression = metrics.compression.inputTokens > 0 ? `${Math.round((1 - metrics.compression.ratio) * 100)}% ${metrics.compression.strategy.toUpperCase()}` : '';
|
|
988
991
|
const phase = busy && currentTool ? phaseFor(currentTool) : (busy ? 'working' : 'ready');
|
|
989
992
|
const elapsedMs = busy && runStartedAt.current !== null ? Math.max(0, now - runStartedAt.current) : 0;
|
|
@@ -1009,7 +1012,7 @@ export function TerminalInterface({ engine }) {
|
|
|
1009
1012
|
const liveThinkView = liveThinkLines.slice(-Math.max(2, Math.floor(liveBudget / 2)));
|
|
1010
1013
|
const liveAnswerView = liveAnswerLines.slice(-liveBudget);
|
|
1011
1014
|
const doneAgents = agents.filter((lane) => lane.status === 'done').length;
|
|
1012
|
-
const panel = _jsx(SidebarPanel, { width: SIDEBAR_WIDTH, model: engine.state.activeModel, mode: mode, perm: permMode, workspace: engine.state.workspace.root, usage: { tokensIn: metrics.
|
|
1015
|
+
const panel = _jsx(SidebarPanel, { width: SIDEBAR_WIDTH, model: engine.state.activeModel, mode: mode, perm: permMode, workspace: engine.state.workspace.root, usage: { tokensIn: metrics.usage?.tokensIn, tokensOut: metrics.usage?.tokensOut, costUSD: metrics.usage?.costUsd, requests: metrics.requestCount }, agents: agents, todos: taskQueue, skills: engine.skills.length, plugins: new Set(engine.skills.map((skill) => skill.source).filter((source) => source !== undefined)).size });
|
|
1013
1016
|
return _jsxs(Box, { flexDirection: "column", width: width, paddingLeft: gutter, paddingRight: gutter, children: [_jsx(Static, { items: lines, children: (line, index) => _jsx(TranscriptEntry, { line: line, width: contentWidth, fallbackModel: engine.state.activeModel }, index) }, staticKey), sideMode === 'replace' && _jsx(Box, { marginBottom: 1, children: panel }), _jsxs(Box, { flexDirection: "row", alignItems: "flex-start", children: [_jsxs(Box, { flexDirection: "column", width: sideMode === 'split' ? convoWidth : undefined, flexGrow: sideMode === 'split' ? 0 : 1, children: [view.showHero && _jsx(Hero, { width: contentWidth + 2, endpoint: engine.client.endpoint ?? 'omniroute', model: engine.state.activeModel, mode: mode, perm: permMode, workspace: engine.state.workspace.root, sessions: recentSessions, skills: engine.skills.length, plugins: new Set(engine.skills.map((skill) => skill.source).filter((source) => source !== undefined)).size, mcpTools: engine.mcpTools.length }), liveThink !== '' && _jsxs(Box, { flexDirection: "column", marginTop: 1, children: [_jsx(Text, { bold: true, color: PALETTE.warn, children: "\u00B7 thinking" }), liveThinkView.map((segments, index) => _jsx(SegmentText, { segments: segments, role: "thinking" }, index))] }), liveAnswer !== '' && _jsxs(Box, { flexDirection: "column", marginTop: 1, children: [_jsx(Text, { bold: true, color: PALETTE.accent, children: engine.state.activeModel }), liveAnswerView.map((segments, index) => _jsx(SegmentText, { segments: segments, role: "assistant" }, index))] }), (view.toolCards > 0 ? toolCards.slice(-view.toolCards) : []).map((card) => {
|
|
1014
1017
|
const expanded = expandedTool === card.id;
|
|
1015
1018
|
const status = card.status === 'running' ? 'running' : card.status === 'error' ? 'error' : 'done';
|