@link-assistant/agent 0.8.5 → 0.8.7
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/package.json +1 -1
- package/src/session/index.ts +154 -6
- package/src/session/processor.ts +15 -4
package/package.json
CHANGED
package/src/session/index.ts
CHANGED
|
@@ -408,9 +408,38 @@ export namespace Session {
|
|
|
408
408
|
}
|
|
409
409
|
|
|
410
410
|
try {
|
|
411
|
-
// Handle undefined/null
|
|
411
|
+
// Handle undefined/null gracefully by returning 0
|
|
412
|
+
// These are expected for optional fields like cachedInputTokens, reasoningTokens
|
|
413
|
+
// See: https://github.com/link-assistant/agent/issues/127
|
|
412
414
|
if (value === undefined || value === null) {
|
|
413
|
-
|
|
415
|
+
if (Flag.OPENCODE_VERBOSE) {
|
|
416
|
+
log.debug(() => ({
|
|
417
|
+
message: 'toNumber received undefined/null, returning 0',
|
|
418
|
+
context,
|
|
419
|
+
valueType: typeof value,
|
|
420
|
+
}));
|
|
421
|
+
}
|
|
422
|
+
return 0;
|
|
423
|
+
}
|
|
424
|
+
|
|
425
|
+
// Handle objects with a 'total' field (e.g., { total: 8707, noCache: 6339, cacheRead: 2368 })
|
|
426
|
+
// Some AI providers return token counts as objects instead of plain numbers
|
|
427
|
+
// See: https://github.com/link-assistant/agent/issues/125
|
|
428
|
+
if (
|
|
429
|
+
typeof value === 'object' &&
|
|
430
|
+
value !== null &&
|
|
431
|
+
'total' in value &&
|
|
432
|
+
typeof (value as { total: unknown }).total === 'number'
|
|
433
|
+
) {
|
|
434
|
+
const result = (value as { total: number }).total;
|
|
435
|
+
if (Flag.OPENCODE_VERBOSE) {
|
|
436
|
+
log.debug(() => ({
|
|
437
|
+
message: 'toNumber extracted total from object',
|
|
438
|
+
context,
|
|
439
|
+
result,
|
|
440
|
+
}));
|
|
441
|
+
}
|
|
442
|
+
return result;
|
|
414
443
|
}
|
|
415
444
|
|
|
416
445
|
// Try to convert to number
|
|
@@ -448,6 +477,90 @@ export namespace Session {
|
|
|
448
477
|
}
|
|
449
478
|
};
|
|
450
479
|
|
|
480
|
+
/**
|
|
481
|
+
* Safely converts a finishReason value to a string.
|
|
482
|
+
* Some AI providers return finishReason as an object instead of a string.
|
|
483
|
+
*
|
|
484
|
+
* For example, OpenCode provider on certain Bun versions may return:
|
|
485
|
+
* - { type: "stop" } instead of "stop"
|
|
486
|
+
* - { finishReason: "tool-calls" } instead of "tool-calls"
|
|
487
|
+
*
|
|
488
|
+
* This function handles these cases gracefully.
|
|
489
|
+
*
|
|
490
|
+
* @param value - The finishReason value (string, object, or undefined)
|
|
491
|
+
* @returns A string representing the finish reason, or 'unknown' if conversion fails
|
|
492
|
+
* @see https://github.com/link-assistant/agent/issues/125
|
|
493
|
+
*/
|
|
494
|
+
export const toFinishReason = (value: unknown): string => {
|
|
495
|
+
// Log input data in verbose mode to help identify issues
|
|
496
|
+
if (Flag.OPENCODE_VERBOSE) {
|
|
497
|
+
log.debug(() => ({
|
|
498
|
+
message: 'toFinishReason input',
|
|
499
|
+
valueType: typeof value,
|
|
500
|
+
value:
|
|
501
|
+
typeof value === 'object' ? JSON.stringify(value) : String(value),
|
|
502
|
+
}));
|
|
503
|
+
}
|
|
504
|
+
|
|
505
|
+
// If it's already a string, return it
|
|
506
|
+
if (typeof value === 'string') {
|
|
507
|
+
return value;
|
|
508
|
+
}
|
|
509
|
+
|
|
510
|
+
// If it's undefined or null, return 'unknown'
|
|
511
|
+
if (value === undefined || value === null) {
|
|
512
|
+
return 'unknown';
|
|
513
|
+
}
|
|
514
|
+
|
|
515
|
+
// If it's an object, try to extract a meaningful string
|
|
516
|
+
if (typeof value === 'object') {
|
|
517
|
+
const obj = value as Record<string, unknown>;
|
|
518
|
+
|
|
519
|
+
// Try common field names that might contain the reason
|
|
520
|
+
if (typeof obj.type === 'string') {
|
|
521
|
+
if (Flag.OPENCODE_VERBOSE) {
|
|
522
|
+
log.debug(() => ({
|
|
523
|
+
message: 'toFinishReason extracted type from object',
|
|
524
|
+
result: obj.type,
|
|
525
|
+
}));
|
|
526
|
+
}
|
|
527
|
+
return obj.type;
|
|
528
|
+
}
|
|
529
|
+
|
|
530
|
+
if (typeof obj.finishReason === 'string') {
|
|
531
|
+
if (Flag.OPENCODE_VERBOSE) {
|
|
532
|
+
log.debug(() => ({
|
|
533
|
+
message: 'toFinishReason extracted finishReason from object',
|
|
534
|
+
result: obj.finishReason,
|
|
535
|
+
}));
|
|
536
|
+
}
|
|
537
|
+
return obj.finishReason;
|
|
538
|
+
}
|
|
539
|
+
|
|
540
|
+
if (typeof obj.reason === 'string') {
|
|
541
|
+
if (Flag.OPENCODE_VERBOSE) {
|
|
542
|
+
log.debug(() => ({
|
|
543
|
+
message: 'toFinishReason extracted reason from object',
|
|
544
|
+
result: obj.reason,
|
|
545
|
+
}));
|
|
546
|
+
}
|
|
547
|
+
return obj.reason;
|
|
548
|
+
}
|
|
549
|
+
|
|
550
|
+
// If we can't extract a specific field, return JSON representation
|
|
551
|
+
if (Flag.OPENCODE_VERBOSE) {
|
|
552
|
+
log.debug(() => ({
|
|
553
|
+
message: 'toFinishReason could not extract string, using JSON',
|
|
554
|
+
result: JSON.stringify(value),
|
|
555
|
+
}));
|
|
556
|
+
}
|
|
557
|
+
return JSON.stringify(value);
|
|
558
|
+
}
|
|
559
|
+
|
|
560
|
+
// For any other type, convert to string
|
|
561
|
+
return String(value);
|
|
562
|
+
};
|
|
563
|
+
|
|
451
564
|
export const getUsage = fn(
|
|
452
565
|
z.object({
|
|
453
566
|
model: z.custom<ModelsDev.Model>(),
|
|
@@ -468,9 +581,28 @@ export namespace Session {
|
|
|
468
581
|
const safeNum = (n: number): number =>
|
|
469
582
|
Number.isNaN(n) || !Number.isFinite(n) ? 0 : n;
|
|
470
583
|
|
|
471
|
-
|
|
584
|
+
// Extract top-level cachedInputTokens
|
|
585
|
+
const topLevelCachedInputTokens = safeNum(
|
|
472
586
|
toNumber(input.usage.cachedInputTokens, 'cachedInputTokens')
|
|
473
587
|
);
|
|
588
|
+
|
|
589
|
+
// Some providers (e.g., opencode/grok-code) nest cacheRead inside inputTokens object
|
|
590
|
+
// e.g., inputTokens: { total: 12703, noCache: 12511, cacheRead: 192 }
|
|
591
|
+
// See: https://github.com/link-assistant/agent/issues/127
|
|
592
|
+
const inputTokensObj = input.usage.inputTokens;
|
|
593
|
+
const nestedCacheRead =
|
|
594
|
+
typeof inputTokensObj === 'object' && inputTokensObj !== null
|
|
595
|
+
? safeNum(
|
|
596
|
+
toNumber(
|
|
597
|
+
(inputTokensObj as { cacheRead?: unknown }).cacheRead,
|
|
598
|
+
'inputTokens.cacheRead'
|
|
599
|
+
)
|
|
600
|
+
)
|
|
601
|
+
: 0;
|
|
602
|
+
|
|
603
|
+
// Use top-level if available, otherwise fall back to nested
|
|
604
|
+
const cachedInputTokens = topLevelCachedInputTokens || nestedCacheRead;
|
|
605
|
+
|
|
474
606
|
const excludesCachedTokens = !!(
|
|
475
607
|
input.metadata?.['anthropic'] || input.metadata?.['bedrock']
|
|
476
608
|
);
|
|
@@ -491,12 +623,28 @@ export namespace Session {
|
|
|
491
623
|
)
|
|
492
624
|
);
|
|
493
625
|
|
|
626
|
+
// Extract reasoning tokens - some providers nest it inside outputTokens
|
|
627
|
+
// e.g., outputTokens: { total: 562, text: -805, reasoning: 1367 }
|
|
628
|
+
// See: https://github.com/link-assistant/agent/issues/127
|
|
629
|
+
const topLevelReasoningTokens = safeNum(
|
|
630
|
+
toNumber(input.usage?.reasoningTokens, 'reasoningTokens')
|
|
631
|
+
);
|
|
632
|
+
const outputTokensObj = input.usage.outputTokens;
|
|
633
|
+
const nestedReasoning =
|
|
634
|
+
typeof outputTokensObj === 'object' && outputTokensObj !== null
|
|
635
|
+
? safeNum(
|
|
636
|
+
toNumber(
|
|
637
|
+
(outputTokensObj as { reasoning?: unknown }).reasoning,
|
|
638
|
+
'outputTokens.reasoning'
|
|
639
|
+
)
|
|
640
|
+
)
|
|
641
|
+
: 0;
|
|
642
|
+
const reasoningTokens = topLevelReasoningTokens || nestedReasoning;
|
|
643
|
+
|
|
494
644
|
const tokens = {
|
|
495
645
|
input: Math.max(0, adjustedInputTokens), // Ensure non-negative
|
|
496
646
|
output: safeNum(toNumber(input.usage.outputTokens, 'outputTokens')),
|
|
497
|
-
reasoning:
|
|
498
|
-
toNumber(input.usage?.reasoningTokens, 'reasoningTokens')
|
|
499
|
-
),
|
|
647
|
+
reasoning: reasoningTokens,
|
|
500
648
|
cache: {
|
|
501
649
|
write: cacheWriteTokens,
|
|
502
650
|
read: cachedInputTokens,
|
package/src/session/processor.ts
CHANGED
|
@@ -224,12 +224,17 @@ export namespace SessionProcessor {
|
|
|
224
224
|
usage: value.usage,
|
|
225
225
|
metadata: value.providerMetadata,
|
|
226
226
|
});
|
|
227
|
-
|
|
227
|
+
// Use toFinishReason to safely convert object/string finishReason to string
|
|
228
|
+
// See: https://github.com/link-assistant/agent/issues/125
|
|
229
|
+
const finishReason = Session.toFinishReason(
|
|
230
|
+
value.finishReason
|
|
231
|
+
);
|
|
232
|
+
input.assistantMessage.finish = finishReason;
|
|
228
233
|
input.assistantMessage.cost += usage.cost;
|
|
229
234
|
input.assistantMessage.tokens = usage.tokens;
|
|
230
235
|
await Session.updatePart({
|
|
231
236
|
id: Identifier.ascending('part'),
|
|
232
|
-
reason:
|
|
237
|
+
reason: finishReason,
|
|
233
238
|
snapshot: await Snapshot.track(),
|
|
234
239
|
messageID: input.assistantMessage.id,
|
|
235
240
|
sessionID: input.assistantMessage.sessionID,
|
|
@@ -274,13 +279,19 @@ export namespace SessionProcessor {
|
|
|
274
279
|
|
|
275
280
|
case 'text-delta':
|
|
276
281
|
if (currentText) {
|
|
277
|
-
|
|
282
|
+
// Handle case where value.text might be an object instead of string
|
|
283
|
+
// See: https://github.com/link-assistant/agent/issues/125
|
|
284
|
+
const textDelta =
|
|
285
|
+
typeof value.text === 'string'
|
|
286
|
+
? value.text
|
|
287
|
+
: String(value.text);
|
|
288
|
+
currentText.text += textDelta;
|
|
278
289
|
if (value.providerMetadata)
|
|
279
290
|
currentText.metadata = value.providerMetadata;
|
|
280
291
|
if (currentText.text)
|
|
281
292
|
await Session.updatePart({
|
|
282
293
|
part: currentText,
|
|
283
|
-
delta:
|
|
294
|
+
delta: textDelta,
|
|
284
295
|
});
|
|
285
296
|
}
|
|
286
297
|
break;
|