@link-assistant/agent 0.16.1 → 0.16.2

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 CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@link-assistant/agent",
3
- "version": "0.16.1",
3
+ "version": "0.16.2",
4
4
  "description": "A minimal, public domain AI CLI agent compatible with OpenCode's JSON interface. Bun-only runtime.",
5
5
  "main": "src/index.js",
6
6
  "type": "module",
@@ -614,15 +614,59 @@ export namespace Session {
614
614
  const safeNum = (n: number): number =>
615
615
  Number.isNaN(n) || !Number.isFinite(n) ? 0 : n;
616
616
 
617
+ // Check if standard usage has valid data (inputTokens or outputTokens defined)
618
+ // If not, try to extract from providerMetadata.openrouter.usage
619
+ // This handles cases where OpenRouter-compatible APIs (like Kilo) put usage in metadata
620
+ // See: https://github.com/link-assistant/agent/issues/187
621
+ const openrouterUsage = input.metadata?.['openrouter']?.['usage'] as
622
+ | {
623
+ promptTokens?: number;
624
+ completionTokens?: number;
625
+ totalTokens?: number;
626
+ cost?: number;
627
+ promptTokensDetails?: { cachedTokens?: number };
628
+ completionTokensDetails?: { reasoningTokens?: number };
629
+ costDetails?: { upstreamInferenceCost?: number };
630
+ }
631
+ | undefined;
632
+
633
+ const standardUsageIsEmpty =
634
+ input.usage.inputTokens === undefined &&
635
+ input.usage.outputTokens === undefined;
636
+
637
+ // If standard usage is empty but openrouter metadata has usage, use it as source
638
+ let effectiveUsage = input.usage;
639
+ if (standardUsageIsEmpty && openrouterUsage) {
640
+ if (Flag.OPENCODE_VERBOSE) {
641
+ log.debug(() => ({
642
+ message:
643
+ 'Standard usage empty, falling back to openrouter metadata',
644
+ openrouterUsage: JSON.stringify(openrouterUsage),
645
+ }));
646
+ }
647
+ // Create a usage-like object from openrouter metadata
648
+ // The openrouter usage uses camelCase: promptTokens, completionTokens
649
+ effectiveUsage = {
650
+ ...input.usage,
651
+ inputTokens: openrouterUsage.promptTokens,
652
+ outputTokens: openrouterUsage.completionTokens,
653
+ totalTokens: openrouterUsage.totalTokens,
654
+ cachedInputTokens:
655
+ openrouterUsage.promptTokensDetails?.cachedTokens ?? 0,
656
+ reasoningTokens:
657
+ openrouterUsage.completionTokensDetails?.reasoningTokens ?? 0,
658
+ };
659
+ }
660
+
617
661
  // Extract top-level cachedInputTokens
618
662
  const topLevelCachedInputTokens = safeNum(
619
- toNumber(input.usage.cachedInputTokens, 'cachedInputTokens')
663
+ toNumber(effectiveUsage.cachedInputTokens, 'cachedInputTokens')
620
664
  );
621
665
 
622
666
  // Some providers (e.g., opencode/grok-code) nest cacheRead inside inputTokens object
623
667
  // e.g., inputTokens: { total: 12703, noCache: 12511, cacheRead: 192 }
624
668
  // See: https://github.com/link-assistant/agent/issues/127
625
- const inputTokensObj = input.usage.inputTokens;
669
+ const inputTokensObj = effectiveUsage.inputTokens;
626
670
  const nestedCacheRead =
627
671
  typeof inputTokensObj === 'object' && inputTokensObj !== null
628
672
  ? safeNum(
@@ -641,7 +685,7 @@ export namespace Session {
641
685
  );
642
686
 
643
687
  const rawInputTokens = safeNum(
644
- toNumber(input.usage.inputTokens, 'inputTokens')
688
+ toNumber(effectiveUsage.inputTokens, 'inputTokens')
645
689
  );
646
690
  const adjustedInputTokens = excludesCachedTokens
647
691
  ? rawInputTokens
@@ -660,9 +704,9 @@ export namespace Session {
660
704
  // e.g., outputTokens: { total: 562, text: -805, reasoning: 1367 }
661
705
  // See: https://github.com/link-assistant/agent/issues/127
662
706
  const topLevelReasoningTokens = safeNum(
663
- toNumber(input.usage?.reasoningTokens, 'reasoningTokens')
707
+ toNumber(effectiveUsage?.reasoningTokens, 'reasoningTokens')
664
708
  );
665
- const outputTokensObj = input.usage.outputTokens;
709
+ const outputTokensObj = effectiveUsage.outputTokens;
666
710
  const nestedReasoning =
667
711
  typeof outputTokensObj === 'object' && outputTokensObj !== null
668
712
  ? safeNum(
@@ -676,7 +720,7 @@ export namespace Session {
676
720
 
677
721
  const tokens = {
678
722
  input: Math.max(0, adjustedInputTokens), // Ensure non-negative
679
- output: safeNum(toNumber(input.usage.outputTokens, 'outputTokens')),
723
+ output: safeNum(toNumber(effectiveUsage.outputTokens, 'outputTokens')),
680
724
  reasoning: reasoningTokens,
681
725
  cache: {
682
726
  write: cacheWriteTokens,
@@ -256,9 +256,24 @@ export namespace SessionProcessor {
256
256
  });
257
257
  // Use toFinishReason to safely convert object/string finishReason to string
258
258
  // See: https://github.com/link-assistant/agent/issues/125
259
- const finishReason = Session.toFinishReason(
260
- value.finishReason
261
- );
259
+ // Also check providerMetadata for finish reason if undefined
260
+ // OpenRouter-compatible APIs (like Kilo) may not populate finishReason in standard location
261
+ // See: https://github.com/link-assistant/agent/issues/187
262
+ let rawFinishReason = value.finishReason;
263
+ if (rawFinishReason === undefined) {
264
+ // Try to extract from OpenRouter provider metadata
265
+ // The openrouter metadata may contain finish_reason or other indicators
266
+ const openrouterMeta =
267
+ value.providerMetadata?.['openrouter'];
268
+ if (openrouterMeta) {
269
+ // OpenRouter sometimes includes reason in reasoning_details or annotations
270
+ // For now, if we have usage data, we can assume it completed successfully
271
+ if (openrouterMeta['usage']) {
272
+ rawFinishReason = 'stop';
273
+ }
274
+ }
275
+ }
276
+ const finishReason = Session.toFinishReason(rawFinishReason);
262
277
  input.assistantMessage.finish = finishReason;
263
278
  input.assistantMessage.cost += usage.cost;
264
279
  input.assistantMessage.tokens = usage.tokens;