sidekick-shared 0.28.3 → 0.29.0
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/CHANGELOG.md +64 -0
- package/README.md +149 -19
- package/dist/authStatus.d.ts +54 -0
- package/dist/authStatus.js +91 -0
- package/dist/browser.d.ts +12 -6
- package/dist/browser.js +17 -1
- package/dist/codex.d.ts +23 -0
- package/dist/codex.js +44 -0
- package/dist/codexProfiles.d.ts +1 -0
- package/dist/codexProfiles.js +8 -11
- package/dist/esm/authStatus.js +87 -0
- package/dist/esm/browser.js +4 -2
- package/dist/esm/codex.js +18 -0
- package/dist/esm/codexProfiles.js +8 -11
- package/dist/esm/formatting.js +10 -1
- package/dist/esm/index.js +9 -6
- package/dist/esm/modelCatalog.js +2 -1
- package/dist/esm/modelInfo.js +422 -45
- package/dist/esm/node.js +9 -3
- package/dist/esm/parsers/codexParser.js +175 -54
- package/dist/esm/parsers/codexRolloutOrigin.js +107 -0
- package/dist/esm/parsers/openCodeParser.js +83 -21
- package/dist/esm/pricing.js +18 -0
- package/dist/esm/pricingCatalog.js +14 -7
- package/dist/esm/promptHistory.js +66 -4
- package/dist/esm/providerFailure.js +124 -3
- package/dist/esm/providers/codex.js +2 -24
- package/dist/esm/providers/codexDatabase.js +3 -19
- package/dist/esm/providers/codexRollouts.js +24 -1
- package/dist/esm/providers/openCode.js +9 -17
- package/dist/esm/report/htmlReportGenerator.js +42 -5
- package/dist/esm/report/transcriptParser.js +13 -11
- package/dist/esm/schemas/providerFailure.js +3 -1
- package/dist/esm/schemas/transcript.js +4 -0
- package/dist/esm/sessionPromptHistory.js +2 -2
- package/dist/esm/statusline/payload.js +9 -0
- package/dist/esm/testing/credentialGuard.js +87 -5
- package/dist/esm/toolCallOrigins.js +310 -0
- package/dist/esm/transcript.js +42 -3
- package/dist/esm/usage/usageEvents.js +146 -17
- package/dist/esm/usage/usageReports.js +39 -6
- package/dist/esm/watchers/jsonlTail.js +378 -59
- package/dist/formatting.d.ts +26 -1
- package/dist/formatting.js +11 -1
- package/dist/index.d.ts +16 -10
- package/dist/index.js +35 -12
- package/dist/modelCatalog.js +2 -1
- package/dist/modelInfo.d.ts +185 -16
- package/dist/modelInfo.js +431 -47
- package/dist/node.d.ts +16 -5
- package/dist/node.js +31 -2
- package/dist/parsers/codexParser.d.ts +55 -0
- package/dist/parsers/codexParser.js +175 -54
- package/dist/parsers/codexRolloutOrigin.d.ts +49 -0
- package/dist/parsers/codexRolloutOrigin.js +111 -0
- package/dist/parsers/openCodeParser.d.ts +38 -0
- package/dist/parsers/openCodeParser.js +86 -21
- package/dist/pricing.d.ts +22 -0
- package/dist/pricing.js +50 -0
- package/dist/pricingCatalog.d.ts +10 -0
- package/dist/pricingCatalog.js +13 -6
- package/dist/promptHistory.d.ts +65 -4
- package/dist/promptHistory.js +68 -5
- package/dist/providerFailure.d.ts +44 -0
- package/dist/providerFailure.js +126 -4
- package/dist/providers/codex.d.ts +3 -22
- package/dist/providers/codex.js +3 -26
- package/dist/providers/codexDatabase.d.ts +3 -2
- package/dist/providers/codexDatabase.js +4 -22
- package/dist/providers/codexRollouts.d.ts +22 -0
- package/dist/providers/codexRollouts.js +24 -0
- package/dist/providers/openCode.js +8 -16
- package/dist/report/htmlReportGenerator.js +42 -5
- package/dist/report/transcriptParser.d.ts +10 -1
- package/dist/report/transcriptParser.js +13 -11
- package/dist/schemas/providerFailure.d.ts +39 -0
- package/dist/schemas/providerFailure.js +2 -0
- package/dist/schemas/transcript.d.ts +2 -0
- package/dist/schemas/transcript.js +4 -0
- package/dist/sessionPromptHistory.d.ts +7 -1
- package/dist/sessionPromptHistory.js +1 -1
- package/dist/statusline/payload.d.ts +10 -0
- package/dist/statusline/payload.js +14 -0
- package/dist/testing/credentialGuard.d.ts +14 -0
- package/dist/testing/credentialGuard.js +87 -5
- package/dist/toolCallOrigins.d.ts +70 -0
- package/dist/toolCallOrigins.js +346 -0
- package/dist/transcript.d.ts +12 -0
- package/dist/transcript.js +42 -3
- package/dist/types/opencode.d.ts +2 -0
- package/dist/usage/usageEvents.d.ts +54 -6
- package/dist/usage/usageEvents.js +146 -17
- package/dist/usage/usageReports.d.ts +35 -4
- package/dist/usage/usageReports.js +39 -6
- package/dist/watchers/jsonlTail.d.ts +101 -4
- package/dist/watchers/jsonlTail.js +380 -60
- package/package.json +41 -1
|
@@ -171,6 +171,10 @@ export function extractPatchFilePaths(input) {
|
|
|
171
171
|
}
|
|
172
172
|
/** Default cap on each piece of per-call correlation state the parser keeps. */
|
|
173
173
|
export const DEFAULT_MAX_TRACKED_TOOL_CALLS = 4096;
|
|
174
|
+
/** Fixed per-entry cost added to each pending call's byte estimate. */
|
|
175
|
+
const PENDING_ENTRY_OVERHEAD_BYTES = 64;
|
|
176
|
+
/** Prefix of the bookkeeping keys the parser stashes inside tool inputs. */
|
|
177
|
+
const SIDEKICK_INPUT_KEY_PREFIX = '_sidekick';
|
|
174
178
|
/** Adds `key`, refreshing its position, and evicts the oldest past `max`. */
|
|
175
179
|
function rememberBounded(set, key, max) {
|
|
176
180
|
set.delete(key);
|
|
@@ -178,12 +182,31 @@ function rememberBounded(set, key, max) {
|
|
|
178
182
|
while (set.size > max)
|
|
179
183
|
set.delete(set.values().next().value);
|
|
180
184
|
}
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
185
|
+
function stringBytes(...values) {
|
|
186
|
+
let total = 0;
|
|
187
|
+
for (const value of values)
|
|
188
|
+
if (typeof value === 'string')
|
|
189
|
+
total += value.length;
|
|
190
|
+
return total;
|
|
191
|
+
}
|
|
192
|
+
function approxJsonBytes(value) {
|
|
193
|
+
if (value === undefined)
|
|
194
|
+
return 0;
|
|
195
|
+
try {
|
|
196
|
+
return JSON.stringify(value)?.length ?? 0;
|
|
197
|
+
}
|
|
198
|
+
catch {
|
|
199
|
+
return 0;
|
|
200
|
+
}
|
|
201
|
+
}
|
|
202
|
+
/** Only the `_sidekick*` bookkeeping keys of a tool input. */
|
|
203
|
+
function sidekickKeysOnly(input) {
|
|
204
|
+
const kept = {};
|
|
205
|
+
for (const key of Object.keys(input)) {
|
|
206
|
+
if (key.startsWith(SIDEKICK_INPUT_KEY_PREFIX))
|
|
207
|
+
kept[key] = input[key];
|
|
208
|
+
}
|
|
209
|
+
return kept;
|
|
187
210
|
}
|
|
188
211
|
/**
|
|
189
212
|
* Stateful parser for Codex JSONL rollout lines.
|
|
@@ -209,10 +232,35 @@ export class CodexRolloutParser {
|
|
|
209
232
|
emittedToolUseIds = new Set();
|
|
210
233
|
patchExpandedToolUseIds = new Map();
|
|
211
234
|
maxTrackedToolCalls;
|
|
235
|
+
/** Byte budget for pending calls; `null` = unlimited (no accounting). */
|
|
236
|
+
maxPendingBytes;
|
|
237
|
+
metadataOnly;
|
|
238
|
+
/** Pending calls in insertion order, for the byte budget. */
|
|
239
|
+
pendingLedger = new Map();
|
|
240
|
+
pendingBytes = 0;
|
|
241
|
+
evictedByCount = 0;
|
|
242
|
+
evictedByBytes = 0;
|
|
212
243
|
constructor(options = {}) {
|
|
213
244
|
const max = options.maxTrackedToolCalls;
|
|
214
245
|
this.maxTrackedToolCalls =
|
|
215
246
|
typeof max === 'number' && max >= 1 ? Math.floor(max) : DEFAULT_MAX_TRACKED_TOOL_CALLS;
|
|
247
|
+
const maxBytes = options.maxPendingBytes;
|
|
248
|
+
this.maxPendingBytes =
|
|
249
|
+
typeof maxBytes === 'number' && Number.isFinite(maxBytes) && maxBytes >= 1
|
|
250
|
+
? Math.floor(maxBytes)
|
|
251
|
+
: null;
|
|
252
|
+
this.metadataOnly = options.metadataOnly === true;
|
|
253
|
+
}
|
|
254
|
+
/** Counters for the pending tool-call state and what the caps evicted. */
|
|
255
|
+
getPendingStats() {
|
|
256
|
+
return {
|
|
257
|
+
pendingCalls: this.pendingExecCommands.size +
|
|
258
|
+
this.pendingMcpToolCalls.size +
|
|
259
|
+
this.patchExpandedToolUseIds.size,
|
|
260
|
+
pendingBytes: this.maxPendingBytes === null ? null : this.pendingBytes,
|
|
261
|
+
evictedByCount: this.evictedByCount,
|
|
262
|
+
evictedByBytes: this.evictedByBytes,
|
|
263
|
+
};
|
|
216
264
|
}
|
|
217
265
|
/** Get stored session metadata. */
|
|
218
266
|
getSessionMeta() {
|
|
@@ -267,6 +315,72 @@ export class CodexRolloutParser {
|
|
|
267
315
|
this.inPlanMode = false;
|
|
268
316
|
this.emittedToolUseIds.clear();
|
|
269
317
|
this.patchExpandedToolUseIds.clear();
|
|
318
|
+
this.pendingLedger.clear();
|
|
319
|
+
this.pendingBytes = 0;
|
|
320
|
+
this.evictedByCount = 0;
|
|
321
|
+
this.evictedByBytes = 0;
|
|
322
|
+
}
|
|
323
|
+
// --- Pending call state ---
|
|
324
|
+
/**
|
|
325
|
+
* Stores a pending call, refreshing its position, then enforces the count
|
|
326
|
+
* cap on its table and the byte budget across all pending tables, oldest
|
|
327
|
+
* first.
|
|
328
|
+
*/
|
|
329
|
+
putPending(table, callId, value, estimateBytes) {
|
|
330
|
+
this.forgetPendingBytes(table, callId);
|
|
331
|
+
table.delete(callId);
|
|
332
|
+
table.set(callId, value);
|
|
333
|
+
while (table.size > this.maxTrackedToolCalls) {
|
|
334
|
+
const oldest = table.keys().next().value;
|
|
335
|
+
table.delete(oldest);
|
|
336
|
+
this.forgetPendingBytes(table, oldest);
|
|
337
|
+
this.evictedByCount += 1;
|
|
338
|
+
}
|
|
339
|
+
if (this.maxPendingBytes === null)
|
|
340
|
+
return;
|
|
341
|
+
const bytes = PENDING_ENTRY_OVERHEAD_BYTES + estimateBytes();
|
|
342
|
+
this.pendingLedger.set(this.ledgerKey(table, callId), {
|
|
343
|
+
table: table,
|
|
344
|
+
callId,
|
|
345
|
+
bytes,
|
|
346
|
+
});
|
|
347
|
+
this.pendingBytes += bytes;
|
|
348
|
+
while (this.pendingBytes > this.maxPendingBytes && this.pendingLedger.size > 0) {
|
|
349
|
+
const [key, oldest] = this.pendingLedger.entries().next().value;
|
|
350
|
+
this.pendingLedger.delete(key);
|
|
351
|
+
this.pendingBytes -= oldest.bytes;
|
|
352
|
+
oldest.table.delete(oldest.callId);
|
|
353
|
+
this.evictedByBytes += 1;
|
|
354
|
+
}
|
|
355
|
+
}
|
|
356
|
+
/** Removes and returns a pending call. */
|
|
357
|
+
takePending(table, callId) {
|
|
358
|
+
const value = table.get(callId);
|
|
359
|
+
table.delete(callId);
|
|
360
|
+
this.forgetPendingBytes(table, callId);
|
|
361
|
+
return value;
|
|
362
|
+
}
|
|
363
|
+
forgetPendingBytes(table, callId) {
|
|
364
|
+
if (this.maxPendingBytes === null)
|
|
365
|
+
return;
|
|
366
|
+
const key = this.ledgerKey(table, callId);
|
|
367
|
+
const entry = this.pendingLedger.get(key);
|
|
368
|
+
if (!entry)
|
|
369
|
+
return;
|
|
370
|
+
this.pendingLedger.delete(key);
|
|
371
|
+
this.pendingBytes -= entry.bytes;
|
|
372
|
+
}
|
|
373
|
+
ledgerKey(table, callId) {
|
|
374
|
+
const kind = table === this.pendingExecCommands ? 'e' : table === this.pendingMcpToolCalls ? 'm' : 'p';
|
|
375
|
+
return `${kind}\u0000${callId}`;
|
|
376
|
+
}
|
|
377
|
+
/** The emitted tool input: unchanged, or only its `_sidekick*` keys in metadata-only mode. */
|
|
378
|
+
toolInput(input) {
|
|
379
|
+
return this.metadataOnly ? sidekickKeysOnly(input) : input;
|
|
380
|
+
}
|
|
381
|
+
/** The emitted tool-result content: unchanged, or empty in metadata-only mode. */
|
|
382
|
+
toolOutput(content) {
|
|
383
|
+
return this.metadataOnly ? '' : content;
|
|
270
384
|
}
|
|
271
385
|
// --- Handlers ---
|
|
272
386
|
handleSessionMeta(timestamp, payload) {
|
|
@@ -373,7 +487,9 @@ export class CodexRolloutParser {
|
|
|
373
487
|
role: 'assistant',
|
|
374
488
|
id: item.id,
|
|
375
489
|
model: this.currentModel || undefined,
|
|
376
|
-
content: [
|
|
490
|
+
content: [
|
|
491
|
+
{ type: 'thinking', thinking: this.metadataOnly ? '' : summaryTexts.join('\n') },
|
|
492
|
+
],
|
|
377
493
|
},
|
|
378
494
|
timestamp,
|
|
379
495
|
},
|
|
@@ -381,11 +497,13 @@ export class CodexRolloutParser {
|
|
|
381
497
|
}
|
|
382
498
|
handleFunctionCall(timestamp, item) {
|
|
383
499
|
let parsedArgs = {};
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
500
|
+
if (!this.metadataOnly) {
|
|
501
|
+
try {
|
|
502
|
+
parsedArgs = JSON.parse(item.arguments);
|
|
503
|
+
}
|
|
504
|
+
catch {
|
|
505
|
+
parsedArgs = { raw: item.arguments };
|
|
506
|
+
}
|
|
389
507
|
}
|
|
390
508
|
const canonicalName = normalizeCodexToolName(item.name);
|
|
391
509
|
const normalizedInput = normalizeCodexToolInput(canonicalName, item.name, parsedArgs);
|
|
@@ -402,7 +520,7 @@ export class CodexRolloutParser {
|
|
|
402
520
|
type: 'tool_use',
|
|
403
521
|
id: item.call_id,
|
|
404
522
|
name: canonicalName,
|
|
405
|
-
input: normalizedInput,
|
|
523
|
+
input: this.toolInput(normalizedInput),
|
|
406
524
|
},
|
|
407
525
|
],
|
|
408
526
|
},
|
|
@@ -422,7 +540,7 @@ export class CodexRolloutParser {
|
|
|
422
540
|
{
|
|
423
541
|
type: 'tool_result',
|
|
424
542
|
tool_use_id: item.call_id,
|
|
425
|
-
content: item.output,
|
|
543
|
+
content: this.toolOutput(item.output),
|
|
426
544
|
is_error: isError,
|
|
427
545
|
},
|
|
428
546
|
],
|
|
@@ -446,11 +564,11 @@ export class CodexRolloutParser {
|
|
|
446
564
|
type: 'tool_use',
|
|
447
565
|
id: item.call_id,
|
|
448
566
|
name: 'Bash',
|
|
449
|
-
input: {
|
|
567
|
+
input: this.toolInput({
|
|
450
568
|
command,
|
|
451
569
|
workdir: item.action?.workdir,
|
|
452
570
|
_sidekickRawToolName: 'local_shell_call',
|
|
453
|
-
},
|
|
571
|
+
}),
|
|
454
572
|
},
|
|
455
573
|
],
|
|
456
574
|
},
|
|
@@ -464,7 +582,7 @@ export class CodexRolloutParser {
|
|
|
464
582
|
if (filePaths.length === 0)
|
|
465
583
|
return [];
|
|
466
584
|
const toolUseIds = filePaths.map((fp) => `${item.call_id}-${fp}`);
|
|
467
|
-
|
|
585
|
+
this.putPending(this.patchExpandedToolUseIds, item.call_id, toolUseIds, () => stringBytes(item.call_id, ...toolUseIds));
|
|
468
586
|
for (const id of toolUseIds) {
|
|
469
587
|
rememberBounded(this.emittedToolUseIds, id, this.maxTrackedToolCalls);
|
|
470
588
|
}
|
|
@@ -479,10 +597,10 @@ export class CodexRolloutParser {
|
|
|
479
597
|
type: 'tool_use',
|
|
480
598
|
id: `${item.call_id}-${fp}`,
|
|
481
599
|
name: 'Edit',
|
|
482
|
-
input: {
|
|
600
|
+
input: this.toolInput({
|
|
483
601
|
file_path: fp,
|
|
484
602
|
_sidekickRawToolName: 'apply_patch',
|
|
485
|
-
},
|
|
603
|
+
}),
|
|
486
604
|
},
|
|
487
605
|
],
|
|
488
606
|
},
|
|
@@ -490,12 +608,14 @@ export class CodexRolloutParser {
|
|
|
490
608
|
}));
|
|
491
609
|
}
|
|
492
610
|
// Generic custom tool
|
|
493
|
-
let parsedInput;
|
|
494
|
-
|
|
495
|
-
|
|
496
|
-
|
|
497
|
-
|
|
498
|
-
|
|
611
|
+
let parsedInput = {};
|
|
612
|
+
if (!this.metadataOnly) {
|
|
613
|
+
try {
|
|
614
|
+
parsedInput = JSON.parse(item.input);
|
|
615
|
+
}
|
|
616
|
+
catch {
|
|
617
|
+
parsedInput = { raw: item.input };
|
|
618
|
+
}
|
|
499
619
|
}
|
|
500
620
|
const canonicalName = normalizeCodexToolName(item.name);
|
|
501
621
|
const normalizedInput = normalizeCodexToolInput(canonicalName, item.name, parsedInput);
|
|
@@ -512,7 +632,7 @@ export class CodexRolloutParser {
|
|
|
512
632
|
type: 'tool_use',
|
|
513
633
|
id: item.call_id,
|
|
514
634
|
name: canonicalName,
|
|
515
|
-
input: normalizedInput,
|
|
635
|
+
input: this.toolInput(normalizedInput),
|
|
516
636
|
},
|
|
517
637
|
],
|
|
518
638
|
},
|
|
@@ -533,8 +653,9 @@ export class CodexRolloutParser {
|
|
|
533
653
|
catch {
|
|
534
654
|
isError = parsePlainTextFailure(item.output);
|
|
535
655
|
}
|
|
536
|
-
const toolUseIds = this.patchExpandedToolUseIds
|
|
537
|
-
|
|
656
|
+
const toolUseIds = this.takePending(this.patchExpandedToolUseIds, item.call_id) ?? [
|
|
657
|
+
item.call_id,
|
|
658
|
+
];
|
|
538
659
|
return toolUseIds.map((toolUseId) => ({
|
|
539
660
|
type: 'user',
|
|
540
661
|
message: {
|
|
@@ -544,7 +665,7 @@ export class CodexRolloutParser {
|
|
|
544
665
|
{
|
|
545
666
|
type: 'tool_result',
|
|
546
667
|
tool_use_id: toolUseId,
|
|
547
|
-
content: item.output,
|
|
668
|
+
content: this.toolOutput(item.output),
|
|
548
669
|
is_error: isError,
|
|
549
670
|
duration,
|
|
550
671
|
},
|
|
@@ -608,18 +729,15 @@ export class CodexRolloutParser {
|
|
|
608
729
|
// response_item/message events which carry richer metadata (id, content parts, role).
|
|
609
730
|
case 'exec_command_begin': {
|
|
610
731
|
const e = event;
|
|
611
|
-
|
|
612
|
-
call_id: e.call_id,
|
|
613
|
-
command: e.command,
|
|
614
|
-
|
|
615
|
-
timestamp,
|
|
616
|
-
}, this.maxTrackedToolCalls);
|
|
732
|
+
const pending = this.metadataOnly
|
|
733
|
+
? { call_id: e.call_id, command: [], timestamp }
|
|
734
|
+
: { call_id: e.call_id, command: e.command, workdir: e.workdir, timestamp };
|
|
735
|
+
this.putPending(this.pendingExecCommands, e.call_id, pending, () => stringBytes(pending.call_id, pending.workdir, pending.timestamp, ...(Array.isArray(pending.command) ? pending.command : [])));
|
|
617
736
|
return [];
|
|
618
737
|
}
|
|
619
738
|
case 'exec_command_end': {
|
|
620
739
|
const e = event;
|
|
621
|
-
const pending = this.pendingExecCommands
|
|
622
|
-
this.pendingExecCommands.delete(e.call_id);
|
|
740
|
+
const pending = this.takePending(this.pendingExecCommands, e.call_id);
|
|
623
741
|
const command = pending?.command?.join(' ') || '';
|
|
624
742
|
const events = [];
|
|
625
743
|
if (!this.emittedToolUseIds.has(e.call_id)) {
|
|
@@ -635,18 +753,20 @@ export class CodexRolloutParser {
|
|
|
635
753
|
type: 'tool_use',
|
|
636
754
|
id: e.call_id,
|
|
637
755
|
name: 'Bash',
|
|
638
|
-
input: {
|
|
756
|
+
input: this.toolInput({
|
|
639
757
|
command,
|
|
640
758
|
workdir: pending?.workdir,
|
|
641
759
|
_sidekickRawToolName: 'exec_command',
|
|
642
|
-
},
|
|
760
|
+
}),
|
|
643
761
|
},
|
|
644
762
|
],
|
|
645
763
|
},
|
|
646
764
|
timestamp: pending?.timestamp || timestamp,
|
|
647
765
|
});
|
|
648
766
|
}
|
|
649
|
-
const output =
|
|
767
|
+
const output = this.metadataOnly
|
|
768
|
+
? ''
|
|
769
|
+
: [e.stdout, e.stderr].filter(Boolean).join('\n') || '';
|
|
650
770
|
events.push({
|
|
651
771
|
type: 'user',
|
|
652
772
|
message: {
|
|
@@ -668,19 +788,20 @@ export class CodexRolloutParser {
|
|
|
668
788
|
}
|
|
669
789
|
case 'mcp_tool_call_begin': {
|
|
670
790
|
const e = event;
|
|
671
|
-
|
|
791
|
+
const pending = {
|
|
672
792
|
call_id: e.call_id,
|
|
673
793
|
server_name: e.server_name,
|
|
674
794
|
tool_name: e.tool_name,
|
|
675
|
-
arguments: e.arguments,
|
|
795
|
+
arguments: this.metadataOnly ? undefined : e.arguments,
|
|
676
796
|
timestamp,
|
|
677
|
-
}
|
|
797
|
+
};
|
|
798
|
+
this.putPending(this.pendingMcpToolCalls, e.call_id, pending, () => stringBytes(pending.call_id, pending.server_name, pending.tool_name, timestamp) +
|
|
799
|
+
approxJsonBytes(pending.arguments));
|
|
678
800
|
return [];
|
|
679
801
|
}
|
|
680
802
|
case 'mcp_tool_call_end': {
|
|
681
803
|
const e = event;
|
|
682
|
-
const pendingMcp = this.pendingMcpToolCalls
|
|
683
|
-
this.pendingMcpToolCalls.delete(e.call_id);
|
|
804
|
+
const pendingMcp = this.takePending(this.pendingMcpToolCalls, e.call_id);
|
|
684
805
|
const toolName = pendingMcp?.tool_name || 'McpTool';
|
|
685
806
|
const events = [];
|
|
686
807
|
if (!this.emittedToolUseIds.has(e.call_id)) {
|
|
@@ -696,11 +817,11 @@ export class CodexRolloutParser {
|
|
|
696
817
|
type: 'tool_use',
|
|
697
818
|
id: e.call_id,
|
|
698
819
|
name: normalizeCodexToolName(toolName),
|
|
699
|
-
input: {
|
|
820
|
+
input: this.toolInput({
|
|
700
821
|
...(pendingMcp?.arguments || {}),
|
|
701
822
|
_sidekickRawToolName: toolName,
|
|
702
823
|
_sidekickMcpServerName: pendingMcp?.server_name,
|
|
703
|
-
},
|
|
824
|
+
}),
|
|
704
825
|
},
|
|
705
826
|
],
|
|
706
827
|
},
|
|
@@ -716,7 +837,7 @@ export class CodexRolloutParser {
|
|
|
716
837
|
{
|
|
717
838
|
type: 'tool_result',
|
|
718
839
|
tool_use_id: e.call_id,
|
|
719
|
-
content: e.result || '',
|
|
840
|
+
content: this.toolOutput(e.result || ''),
|
|
720
841
|
is_error: e.is_error || false,
|
|
721
842
|
duration: e.duration_ms,
|
|
722
843
|
},
|
|
@@ -775,12 +896,12 @@ export class CodexRolloutParser {
|
|
|
775
896
|
type: 'tool_use',
|
|
776
897
|
id: patchId,
|
|
777
898
|
name: 'Edit',
|
|
778
|
-
input: {
|
|
899
|
+
input: this.toolInput({
|
|
779
900
|
file_path: e.file_path,
|
|
780
901
|
additions: e.additions ?? 0,
|
|
781
902
|
deletions: e.deletions ?? 0,
|
|
782
903
|
_sidekickRawToolName: 'patch_applied',
|
|
783
|
-
},
|
|
904
|
+
}),
|
|
784
905
|
},
|
|
785
906
|
],
|
|
786
907
|
},
|
|
@@ -807,10 +928,10 @@ export class CodexRolloutParser {
|
|
|
807
928
|
type: 'tool_use',
|
|
808
929
|
id: `plan-enter-${timestamp}`,
|
|
809
930
|
name: 'EnterPlanMode',
|
|
810
|
-
input: {
|
|
931
|
+
input: this.toolInput({
|
|
811
932
|
source: 'codex_task_started',
|
|
812
933
|
_sidekickRawToolName: 'task_started',
|
|
813
|
-
},
|
|
934
|
+
}),
|
|
814
935
|
},
|
|
815
936
|
],
|
|
816
937
|
},
|
|
@@ -834,10 +955,10 @@ export class CodexRolloutParser {
|
|
|
834
955
|
type: 'tool_use',
|
|
835
956
|
id: `plan-exit-${timestamp}`,
|
|
836
957
|
name: 'ExitPlanMode',
|
|
837
|
-
input: {
|
|
958
|
+
input: this.toolInput({
|
|
838
959
|
source: 'codex_task_complete',
|
|
839
960
|
_sidekickRawToolName: 'task_complete',
|
|
840
|
-
},
|
|
961
|
+
}),
|
|
841
962
|
},
|
|
842
963
|
],
|
|
843
964
|
},
|
|
@@ -0,0 +1,107 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Where a Codex rollout came from, read from its `session_meta` record:
|
|
3
|
+
* a session a person started, a fork of another session, or a subagent.
|
|
4
|
+
*
|
|
5
|
+
* Codex writes `session_meta.source` as a plain string for sessions started
|
|
6
|
+
* from a surface (`cli`, `vscode`, `exec`, …) and as an object for subagents:
|
|
7
|
+
* `{ subagent: { thread_spawn: { parent_thread_id, … } } }` for a spawned
|
|
8
|
+
* agent, `{ subagent: { other: 'guardian' } }` for internal reviewers, or
|
|
9
|
+
* `{ subagent: 'review' }`. The same JSON appears as a string in the state
|
|
10
|
+
* database's `threads.source`. A forked rollout records `forked_from_id`, and
|
|
11
|
+
* embeds its parent's `session_meta` after its own, so only the first counts.
|
|
12
|
+
*
|
|
13
|
+
* Pure: no filesystem access. Browser-safe.
|
|
14
|
+
*
|
|
15
|
+
* @module parsers/codexRolloutOrigin
|
|
16
|
+
*/
|
|
17
|
+
function record(value) {
|
|
18
|
+
return typeof value === 'object' && value !== null && !Array.isArray(value)
|
|
19
|
+
? value
|
|
20
|
+
: undefined;
|
|
21
|
+
}
|
|
22
|
+
function nonEmpty(value) {
|
|
23
|
+
return typeof value === 'string' && value.length > 0 ? value : undefined;
|
|
24
|
+
}
|
|
25
|
+
/** A `source` value as an object: rollouts store it parsed, the database as JSON text. */
|
|
26
|
+
function sourceObject(source) {
|
|
27
|
+
if (typeof source !== 'string')
|
|
28
|
+
return record(source);
|
|
29
|
+
if (!source.startsWith('{'))
|
|
30
|
+
return undefined;
|
|
31
|
+
try {
|
|
32
|
+
return record(JSON.parse(source));
|
|
33
|
+
}
|
|
34
|
+
catch {
|
|
35
|
+
return undefined;
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
/**
|
|
39
|
+
* Parent thread id of a spawned subagent's `source` (object, or JSON text as
|
|
40
|
+
* stored in `threads.source`): `subagent.thread_spawn.parent_thread_id`.
|
|
41
|
+
*/
|
|
42
|
+
export function codexSpawnParentFromSource(source) {
|
|
43
|
+
const spawn = record(record(sourceObject(source)?.subagent)?.thread_spawn);
|
|
44
|
+
const parent = spawn?.parent_thread_id;
|
|
45
|
+
return typeof parent === 'string' ? parent : undefined;
|
|
46
|
+
}
|
|
47
|
+
function subagentKindOf(subagent) {
|
|
48
|
+
if (typeof subagent === 'string')
|
|
49
|
+
return nonEmpty(subagent);
|
|
50
|
+
const variant = record(subagent);
|
|
51
|
+
if (!variant)
|
|
52
|
+
return undefined;
|
|
53
|
+
const other = nonEmpty(variant.other);
|
|
54
|
+
if (other)
|
|
55
|
+
return other;
|
|
56
|
+
return Object.keys(variant)[0];
|
|
57
|
+
}
|
|
58
|
+
/** The `session_meta` payload of a record, of a bare payload, or of the first in a list. */
|
|
59
|
+
function metaPayload(input) {
|
|
60
|
+
if (Array.isArray(input)) {
|
|
61
|
+
for (const item of input) {
|
|
62
|
+
const row = record(item);
|
|
63
|
+
if (row?.type === 'session_meta')
|
|
64
|
+
return record(row.payload);
|
|
65
|
+
}
|
|
66
|
+
return undefined;
|
|
67
|
+
}
|
|
68
|
+
const row = record(input);
|
|
69
|
+
if (!row)
|
|
70
|
+
return undefined;
|
|
71
|
+
if (row.type === 'session_meta')
|
|
72
|
+
return record(row.payload);
|
|
73
|
+
// A payload on its own, as `readSessionMeta()` returns it.
|
|
74
|
+
if (row.type === undefined && row.payload === undefined)
|
|
75
|
+
return row;
|
|
76
|
+
return undefined;
|
|
77
|
+
}
|
|
78
|
+
/**
|
|
79
|
+
* Origin of a Codex rollout from its `session_meta`. Accepts the parsed
|
|
80
|
+
* `session_meta` record, its payload, or the rollout's records in order (the
|
|
81
|
+
* first `session_meta` wins: a fork embeds its parent's after its own).
|
|
82
|
+
* Anything else yields `{ isSubagent: false }`.
|
|
83
|
+
*/
|
|
84
|
+
export function codexRolloutOrigin(input) {
|
|
85
|
+
const payload = metaPayload(input);
|
|
86
|
+
if (!payload)
|
|
87
|
+
return { isSubagent: false };
|
|
88
|
+
const forkedFromId = nonEmpty(payload.forked_from_id);
|
|
89
|
+
const object = sourceObject(payload.source);
|
|
90
|
+
const isSubagent = object !== undefined && 'subagent' in object;
|
|
91
|
+
const source = typeof payload.source === 'string' && object === undefined
|
|
92
|
+
? nonEmpty(payload.source)
|
|
93
|
+
: isSubagent
|
|
94
|
+
? 'subagent'
|
|
95
|
+
: undefined;
|
|
96
|
+
const parentId = forkedFromId ??
|
|
97
|
+
nonEmpty(codexSpawnParentFromSource(object)) ??
|
|
98
|
+
(isSubagent ? nonEmpty(payload.parent_thread_id) : undefined);
|
|
99
|
+
const subagentKind = isSubagent ? subagentKindOf(object?.subagent) : undefined;
|
|
100
|
+
return {
|
|
101
|
+
...(forkedFromId ? { forkedFromId } : {}),
|
|
102
|
+
...(source ? { source } : {}),
|
|
103
|
+
isSubagent,
|
|
104
|
+
...(parentId ? { parentId } : {}),
|
|
105
|
+
...(subagentKind ? { subagentKind } : {}),
|
|
106
|
+
};
|
|
107
|
+
}
|
|
@@ -115,6 +115,78 @@ function toISOString(time) {
|
|
|
115
115
|
return time;
|
|
116
116
|
return new Date(time).toISOString();
|
|
117
117
|
}
|
|
118
|
+
/**
|
|
119
|
+
* Token counts from a message in either storage shape: database rows are
|
|
120
|
+
* mapped to flat `cacheRead` / `cacheWrite`, while file-storage messages keep
|
|
121
|
+
* OpenCode's nested `cache: { read, write }`.
|
|
122
|
+
* @internal
|
|
123
|
+
*/
|
|
124
|
+
export function openCodeTokenCounts(tokens) {
|
|
125
|
+
const raw = (tokens ?? {});
|
|
126
|
+
const cache = (raw.cache ?? {});
|
|
127
|
+
const num = (value) => (typeof value === 'number' ? value : undefined);
|
|
128
|
+
return {
|
|
129
|
+
input: num(raw.input),
|
|
130
|
+
output: num(raw.output),
|
|
131
|
+
reasoning: num(raw.reasoning),
|
|
132
|
+
cacheRead: num(raw.cacheRead) ?? num(cache.read),
|
|
133
|
+
cacheWrite: num(raw.cacheWrite) ?? num(cache.write),
|
|
134
|
+
total: num(raw.total),
|
|
135
|
+
};
|
|
136
|
+
}
|
|
137
|
+
/**
|
|
138
|
+
* How a stored OpenCode message splits its tokens. Current OpenCode stores
|
|
139
|
+
* every bucket disjoint for every provider: `input` excludes cache reads and
|
|
140
|
+
* writes (since 2025-11) and `output` excludes `reasoning` (since 2026-04),
|
|
141
|
+
* with `total` equal to their sum. Older messages either counted cached input
|
|
142
|
+
* in `input` (OpenAI-compatible providers) or reasoning in `output`. The
|
|
143
|
+
* stored `total` tells the layouts apart; without it the current layout is
|
|
144
|
+
* assumed.
|
|
145
|
+
* @internal
|
|
146
|
+
*/
|
|
147
|
+
export function openCodeTokenLayout(tokens) {
|
|
148
|
+
const n = (value) => typeof value === 'number' && Number.isFinite(value) && value > 0 ? value : 0;
|
|
149
|
+
const input = n(tokens.input);
|
|
150
|
+
const output = n(tokens.output);
|
|
151
|
+
const reasoning = n(tokens.reasoning);
|
|
152
|
+
const cache = n(tokens.cacheRead) + n(tokens.cacheWrite);
|
|
153
|
+
const total = n(tokens.total);
|
|
154
|
+
if (total > 0 && total !== input + cache + output + reasoning) {
|
|
155
|
+
if (reasoning > 0 && total === input + cache + output) {
|
|
156
|
+
return { inputIncludesCache: false, reasoningInOutput: true };
|
|
157
|
+
}
|
|
158
|
+
if (cache > 0 && total === input + output + reasoning) {
|
|
159
|
+
return { inputIncludesCache: true, reasoningInOutput: false };
|
|
160
|
+
}
|
|
161
|
+
if (cache > 0 && reasoning > 0 && total === input + output) {
|
|
162
|
+
return { inputIncludesCache: true, reasoningInOutput: true };
|
|
163
|
+
}
|
|
164
|
+
}
|
|
165
|
+
return { inputIncludesCache: false, reasoningInOutput: false };
|
|
166
|
+
}
|
|
167
|
+
/** Normalizes one OpenCode message's or step's stored tokens. @internal */
|
|
168
|
+
export function normalizeOpenCodeUsage(tokens, providerID, extra = {}) {
|
|
169
|
+
const layout = openCodeTokenLayout(tokens);
|
|
170
|
+
return normalizeProviderUsage({
|
|
171
|
+
// `openai` semantics subtract cache reads from input; only an old
|
|
172
|
+
// cache-inclusive message needs that.
|
|
173
|
+
semantics: layout.inputIncludesCache
|
|
174
|
+
? 'openai'
|
|
175
|
+
: providerID === 'anthropic'
|
|
176
|
+
? 'anthropic'
|
|
177
|
+
: 'sidekick',
|
|
178
|
+
provider: providerID ?? 'opencode',
|
|
179
|
+
...(extra.source ? { source: extra.source } : {}),
|
|
180
|
+
...(extra.model ? { model: extra.model } : {}),
|
|
181
|
+
inputTokens: tokens.input,
|
|
182
|
+
outputTokens: tokens.output,
|
|
183
|
+
cacheReadTokens: tokens.cacheRead,
|
|
184
|
+
cacheWriteTokens: tokens.cacheWrite,
|
|
185
|
+
reasoningTokens: tokens.reasoning,
|
|
186
|
+
reasoningIncludedInOutput: layout.reasoningInOutput,
|
|
187
|
+
...(extra.reportedCostUsd !== undefined ? { reportedCostUsd: extra.reportedCostUsd } : {}),
|
|
188
|
+
});
|
|
189
|
+
}
|
|
118
190
|
/**
|
|
119
191
|
* Converts an OpenCode message and its parts into SessionEvent array.
|
|
120
192
|
*
|
|
@@ -320,31 +392,20 @@ function convertAssistantMessage(message, parts) {
|
|
|
320
392
|
: stepFinishCost > 0
|
|
321
393
|
? stepFinishCost
|
|
322
394
|
: undefined;
|
|
323
|
-
const
|
|
324
|
-
|
|
325
|
-
output_tokens: message.tokens.output || 0,
|
|
326
|
-
cache_creation_input_tokens: message.tokens.cacheWrite || 0,
|
|
327
|
-
cache_read_input_tokens: message.tokens.cacheRead || 0,
|
|
328
|
-
reported_cost: reportedCost,
|
|
329
|
-
reasoning_tokens: message.tokens.reasoning || 0,
|
|
330
|
-
};
|
|
331
|
-
const normalizedUsage = normalizeProviderUsage({
|
|
332
|
-
semantics: message.providerID === 'openai'
|
|
333
|
-
? 'openai'
|
|
334
|
-
: message.providerID === 'anthropic'
|
|
335
|
-
? 'anthropic'
|
|
336
|
-
: 'sidekick',
|
|
337
|
-
provider: message.providerID ?? 'opencode',
|
|
395
|
+
const counts = openCodeTokenCounts(message.tokens);
|
|
396
|
+
const normalizedUsage = normalizeOpenCodeUsage(counts, message.providerID, {
|
|
338
397
|
source: 'opencode-message',
|
|
339
398
|
model: message.modelID,
|
|
340
|
-
inputTokens: message.tokens.input,
|
|
341
|
-
outputTokens: message.tokens.output,
|
|
342
|
-
cacheReadTokens: message.tokens.cacheRead,
|
|
343
|
-
cacheWriteTokens: message.tokens.cacheWrite,
|
|
344
|
-
reasoningTokens: message.tokens.reasoning,
|
|
345
|
-
reasoningIncludedInOutput: message.providerID === 'openai',
|
|
346
399
|
reportedCostUsd: reportedCost,
|
|
347
400
|
});
|
|
401
|
+
const usage = {
|
|
402
|
+
input_tokens: normalizedUsage.uncachedInputTokens,
|
|
403
|
+
output_tokens: counts.output || 0,
|
|
404
|
+
cache_creation_input_tokens: counts.cacheWrite || 0,
|
|
405
|
+
cache_read_input_tokens: counts.cacheRead || 0,
|
|
406
|
+
reported_cost: reportedCost,
|
|
407
|
+
reasoning_tokens: counts.reasoning || 0,
|
|
408
|
+
};
|
|
348
409
|
const timestamp = toISOString(message.time.completed || message.time.created);
|
|
349
410
|
const retryAttempts = parts
|
|
350
411
|
.filter((part) => part.type === 'retry')
|
|
@@ -483,6 +544,7 @@ export function parseDbMessageData(row) {
|
|
|
483
544
|
cacheRead: cache?.read || 0,
|
|
484
545
|
cacheWrite: cache?.write || 0,
|
|
485
546
|
reasoning: tokens?.reasoning || 0,
|
|
547
|
+
...(typeof tokens?.total === 'number' ? { total: tokens.total } : {}),
|
|
486
548
|
},
|
|
487
549
|
time: {
|
|
488
550
|
created: time?.created || row.time_created,
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `sidekick-shared/pricing` — model pricing, model-id identity, usage
|
|
3
|
+
* normalization and cost, cost provenance, cost/token formatting, and text
|
|
4
|
+
* token estimates, as one small pure entry (no filesystem, network, or Zod).
|
|
5
|
+
*
|
|
6
|
+
* For hosts that price usage or format costs without session reading: it
|
|
7
|
+
* loads a handful of modules instead of the full browser surface. LiteLLM
|
|
8
|
+
* catalog hydration stays Node-only in `sidekick-shared/node`
|
|
9
|
+
* (`hydratePricingCatalog`); once hydrated, lookups here see the catalog.
|
|
10
|
+
* Everything here is also exported from the package root and
|
|
11
|
+
* `sidekick-shared/browser`.
|
|
12
|
+
*
|
|
13
|
+
* @module pricing
|
|
14
|
+
*/
|
|
15
|
+
export { MODEL_PROVIDER_IDS, canonicalModelId, isMovingModelAlias, parseModelId, compareModelIds, sortModelIds, shortModelName, getModelDisplayInfo, getModelInfo, getModelPricing, resolveModelPricing, registerModelPricing, clearRegisteredModelPricing, getPricingCatalogInfo, calculateCost, calculateCostWithPricing, calculateCostWithProvenance, mergeCostSources, costSourceFromPricingProvenance, mergeCostComponents, formatCost, } from './modelInfo';
|
|
16
|
+
export { normalizeProviderUsage, extractNormalizedUsage, calculateNormalizedUsageCost, } from './usageNormalization';
|
|
17
|
+
export { formatTokenCount, formatDurationMs } from './formatting';
|
|
18
|
+
export { ASCII_CHARS_PER_TOKEN, estimateTextTokens, estimateSerializedTokens, } from './tokenEstimation';
|