All files / src/sdk usage.ts

100% Statements 36/36
100% Branches 52/52
100% Functions 7/7
100% Lines 32/32

Press n or j to go to the next uncovered block, b, p or k for the previous block.

1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140                                                          12x 12x 12x 12x 12x 12x 12x 12x 12x 12x 12x                               207x                         268x                               177x                     23x   9x 9x 16x 16x 5x   11x       9x                     62x 8x 4x   3x 3x 3x   3x 3x   3x               26x    
import type { UsageStats, AuxiliaryUsageMap } from '../types/index.js';
import type { AuxiliaryUsageEntry } from './types.js';
 
export interface RuntimeUsageResult {
  usage?: {
    input_tokens?: number | null;
    output_tokens?: number | null;
    cache_read_input_tokens?: number | null;
    cache_creation_input_tokens?: number | null;
    cache_creation?: {
      ephemeral_1h_input_tokens?: number | null;
      ephemeral_5m_input_tokens?: number | null;
    } | null;
    server_tool_use?: {
      web_search_requests?: number | null;
    } | null;
  } | null;
  total_cost_usd?: number | null;
}
 
/**
 * Extract usage stats from a runtime result message.
 *
 * The Anthropic API reports `input_tokens` as only the non-cached portion.
 * We normalize so that `inputTokens` is the total input token count
 * (non-cached + cache_read + cache_creation), with cache fields reported
 * separately as subsets of that total.
 */
export function extractUsage(result: RuntimeUsageResult): UsageStats {
  const usage = result.usage;
  const rawInput = usage?.input_tokens ?? 0;
  const cacheRead = usage?.cache_read_input_tokens ?? 0;
  const rawCacheCreation = usage?.cache_creation_input_tokens ?? 0;
  const cacheCreation1h = usage?.cache_creation?.ephemeral_1h_input_tokens ?? 0;
  const tieredCacheCreation5m = usage?.cache_creation?.ephemeral_5m_input_tokens ?? 0;
  const hasTieredCacheCreation = usage?.cache_creation !== undefined && usage.cache_creation !== null;
  const tieredCacheCreation = tieredCacheCreation5m + cacheCreation1h;
  const cacheCreation = Math.max(rawCacheCreation, tieredCacheCreation);
  const cacheCreation5m = hasTieredCacheCreation ? tieredCacheCreation5m : rawCacheCreation;
  return {
    inputTokens: rawInput + cacheRead + cacheCreation,
    outputTokens: usage?.output_tokens ?? 0,
    cacheReadInputTokens: cacheRead,
    cacheCreationInputTokens: cacheCreation,
    cacheCreation5mInputTokens: cacheCreation5m,
    cacheCreation1hInputTokens: cacheCreation1h,
    webSearchRequests: usage?.server_tool_use?.web_search_requests ?? 0,
    costUSD: result.total_cost_usd ?? 0,
  };
}
 
/**
 * Create empty usage stats.
 */
export function emptyUsage(): UsageStats {
  return {
    inputTokens: 0,
    outputTokens: 0,
    cacheReadInputTokens: 0,
    cacheCreationInputTokens: 0,
    cacheCreation5mInputTokens: 0,
    cacheCreation1hInputTokens: 0,
    webSearchRequests: 0,
    costUSD: 0,
  };
}
 
function addUsage(a: UsageStats, b: UsageStats): UsageStats {
  return {
    inputTokens: a.inputTokens + b.inputTokens,
    outputTokens: a.outputTokens + b.outputTokens,
    cacheReadInputTokens: (a.cacheReadInputTokens ?? 0) + (b.cacheReadInputTokens ?? 0),
    cacheCreationInputTokens: (a.cacheCreationInputTokens ?? 0) + (b.cacheCreationInputTokens ?? 0),
    cacheCreation5mInputTokens: (a.cacheCreation5mInputTokens ?? 0) + (b.cacheCreation5mInputTokens ?? 0),
    cacheCreation1hInputTokens: (a.cacheCreation1hInputTokens ?? 0) + (b.cacheCreation1hInputTokens ?? 0),
    webSearchRequests: (a.webSearchRequests ?? 0) + (b.webSearchRequests ?? 0),
    costUSD: a.costUSD + b.costUSD,
  };
}
 
/**
 * Aggregate multiple usage stats into one.
 */
export function aggregateUsage(usages: UsageStats[]): UsageStats {
  return usages.reduce(addUsage, emptyUsage());
}
 
/**
 * Aggregate auxiliary usage entries by agent name.
 * Merges multiple entries for the same agent into a single UsageStats.
 * Returns undefined if no entries are provided.
 */
export function aggregateAuxiliaryUsage(
  entries: AuxiliaryUsageEntry[]
): AuxiliaryUsageMap | undefined {
  if (entries.length === 0) return undefined;
 
  const map: AuxiliaryUsageMap = {};
  for (const { agent, usage } of entries) {
    const existing = map[agent];
    if (existing) {
      map[agent] = addUsage(existing, usage);
    } else {
      map[agent] = { ...usage };
    }
  }
 
  return map;
}
 
/**
 * Merge two AuxiliaryUsageMaps together.
 * Entries for the same agent are summed.
 */
export function mergeAuxiliaryUsage(
  a: AuxiliaryUsageMap | undefined,
  b: AuxiliaryUsageMap | undefined
): AuxiliaryUsageMap | undefined {
  if (!a && !b) return undefined;
  if (!a) return b;
  if (!b) return a;
 
  const entries: { agent: string; usage: UsageStats }[] = [];
  for (const [agent, usage] of Object.entries(a)) {
    entries.push({ agent, usage });
  }
  for (const [agent, usage] of Object.entries(b)) {
    entries.push({ agent, usage });
  }
  return aggregateAuxiliaryUsage(entries);
}
 
/**
 * Estimate token count from character count.
 * Uses chars/4 as a rough approximation for English text.
 */
export function estimateTokens(chars: number): number {
  return Math.ceil(chars / 4);
}