langsmith 0.4.8 → 0.4.10

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.
@@ -0,0 +1,102 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.flattenContentBlocks = flattenContentBlocks;
4
+ exports.convertFromAnthropicMessage = convertFromAnthropicMessage;
5
+ exports.isTaskTool = isTaskTool;
6
+ exports.isToolBlock = isToolBlock;
7
+ const utils_js_1 = require("./utils.cjs");
8
+ /**
9
+ * Converts SDK content blocks into serializable objects.
10
+ * Matches Python's flatten_content_blocks behavior.
11
+ */
12
+ function flattenContentBlocks(content) {
13
+ if (!Array.isArray(content)) {
14
+ return content;
15
+ }
16
+ return content.map((block) => {
17
+ if (!block || typeof block !== "object" || !("type" in block)) {
18
+ return block;
19
+ }
20
+ const blockType = block.type;
21
+ switch (blockType) {
22
+ case "text":
23
+ return { type: "text", text: block.text || "" };
24
+ case "thinking":
25
+ return {
26
+ type: "thinking",
27
+ thinking: block.thinking || "",
28
+ signature: block.signature || "",
29
+ };
30
+ case "tool_use":
31
+ return {
32
+ type: "tool_use",
33
+ id: block.id,
34
+ name: block.name,
35
+ input: block.input,
36
+ };
37
+ case "tool_result":
38
+ return {
39
+ type: "tool_result",
40
+ tool_use_id: block.tool_use_id,
41
+ content: block.content,
42
+ is_error: block.is_error || false,
43
+ };
44
+ default:
45
+ return block;
46
+ }
47
+ });
48
+ }
49
+ /**
50
+ * Utility function to convert an Anthropic message to a LangSmith-compatible message.
51
+ * @internal
52
+ */
53
+ function convertFromAnthropicMessage(sdkMessage) {
54
+ if (sdkMessage == null)
55
+ return [];
56
+ if (typeof sdkMessage === "string") {
57
+ return [{ content: sdkMessage, role: "user" }];
58
+ }
59
+ if ((0, utils_js_1.isIterable)(sdkMessage)) {
60
+ return Array.from(sdkMessage).flatMap(convertFromAnthropicMessage);
61
+ }
62
+ if (typeof sdkMessage !== "object" ||
63
+ sdkMessage == null ||
64
+ !("message" in sdkMessage) ||
65
+ (sdkMessage.type !== "assistant" && sdkMessage.type !== "user")) {
66
+ return [];
67
+ }
68
+ const { role = sdkMessage.type, content, ...rest } = sdkMessage.message;
69
+ const flattened = flattenContentBlocks(content);
70
+ const toolResultBlocks = role === "user" && Array.isArray(flattened)
71
+ ? flattened.filter(isToolResultBlock)
72
+ : [];
73
+ if (toolResultBlocks.length > 0) {
74
+ return toolResultBlocks.map((block) => ({ ...block, role: "tool" }));
75
+ }
76
+ return [{ ...rest, content: flattened, role }];
77
+ }
78
+ function isToolResultBlock(block) {
79
+ if (typeof block !== "object" || block == null)
80
+ return false;
81
+ if (!("type" in block))
82
+ return false;
83
+ return block.type === "tool_result";
84
+ }
85
+ /**
86
+ * Type assertion to check if a tool is a Task tool
87
+ * @param tool - The tool to check
88
+ * @returns True if the tool is a Task tool, false otherwise
89
+ * @internal
90
+ */
91
+ function isTaskTool(tool) {
92
+ return tool.type === "tool_use" && tool.name === "Task";
93
+ }
94
+ /**
95
+ * Type-assertion to check for tool blocks
96
+ * @internal
97
+ */
98
+ function isToolBlock(block) {
99
+ if (!block || typeof block !== "object")
100
+ return false;
101
+ return block.type === "tool_use";
102
+ }
@@ -0,0 +1,6 @@
1
+ import type { BetaContentBlock } from "./types.js";
2
+ /**
3
+ * Converts SDK content blocks into serializable objects.
4
+ * Matches Python's flatten_content_blocks behavior.
5
+ */
6
+ export declare function flattenContentBlocks(content: BetaContentBlock[] | unknown): Array<Record<string, unknown>> | unknown;
@@ -0,0 +1,96 @@
1
+ import { isIterable } from "./utils.js";
2
+ /**
3
+ * Converts SDK content blocks into serializable objects.
4
+ * Matches Python's flatten_content_blocks behavior.
5
+ */
6
+ export function flattenContentBlocks(content) {
7
+ if (!Array.isArray(content)) {
8
+ return content;
9
+ }
10
+ return content.map((block) => {
11
+ if (!block || typeof block !== "object" || !("type" in block)) {
12
+ return block;
13
+ }
14
+ const blockType = block.type;
15
+ switch (blockType) {
16
+ case "text":
17
+ return { type: "text", text: block.text || "" };
18
+ case "thinking":
19
+ return {
20
+ type: "thinking",
21
+ thinking: block.thinking || "",
22
+ signature: block.signature || "",
23
+ };
24
+ case "tool_use":
25
+ return {
26
+ type: "tool_use",
27
+ id: block.id,
28
+ name: block.name,
29
+ input: block.input,
30
+ };
31
+ case "tool_result":
32
+ return {
33
+ type: "tool_result",
34
+ tool_use_id: block.tool_use_id,
35
+ content: block.content,
36
+ is_error: block.is_error || false,
37
+ };
38
+ default:
39
+ return block;
40
+ }
41
+ });
42
+ }
43
+ /**
44
+ * Utility function to convert an Anthropic message to a LangSmith-compatible message.
45
+ * @internal
46
+ */
47
+ export function convertFromAnthropicMessage(sdkMessage) {
48
+ if (sdkMessage == null)
49
+ return [];
50
+ if (typeof sdkMessage === "string") {
51
+ return [{ content: sdkMessage, role: "user" }];
52
+ }
53
+ if (isIterable(sdkMessage)) {
54
+ return Array.from(sdkMessage).flatMap(convertFromAnthropicMessage);
55
+ }
56
+ if (typeof sdkMessage !== "object" ||
57
+ sdkMessage == null ||
58
+ !("message" in sdkMessage) ||
59
+ (sdkMessage.type !== "assistant" && sdkMessage.type !== "user")) {
60
+ return [];
61
+ }
62
+ const { role = sdkMessage.type, content, ...rest } = sdkMessage.message;
63
+ const flattened = flattenContentBlocks(content);
64
+ const toolResultBlocks = role === "user" && Array.isArray(flattened)
65
+ ? flattened.filter(isToolResultBlock)
66
+ : [];
67
+ if (toolResultBlocks.length > 0) {
68
+ return toolResultBlocks.map((block) => ({ ...block, role: "tool" }));
69
+ }
70
+ return [{ ...rest, content: flattened, role }];
71
+ }
72
+ function isToolResultBlock(block) {
73
+ if (typeof block !== "object" || block == null)
74
+ return false;
75
+ if (!("type" in block))
76
+ return false;
77
+ return block.type === "tool_result";
78
+ }
79
+ /**
80
+ * Type assertion to check if a tool is a Task tool
81
+ * @param tool - The tool to check
82
+ * @returns True if the tool is a Task tool, false otherwise
83
+ * @internal
84
+ */
85
+ export function isTaskTool(tool) {
86
+ return tool.type === "tool_use" && tool.name === "Task";
87
+ }
88
+ /**
89
+ * Type-assertion to check for tool blocks
90
+ * @internal
91
+ */
92
+ export function isToolBlock(block) {
93
+ if (!block || typeof block !== "object")
94
+ return false;
95
+ return block.type === "tool_use";
96
+ }
@@ -1,2 +1,3 @@
1
1
  "use strict";
2
+ /* eslint-disable @typescript-eslint/no-explicit-any */
2
3
  Object.defineProperty(exports, "__esModule", { value: true });
@@ -1,37 +1,50 @@
1
- import type { RunTree, RunTreeConfig } from "../../run_trees.js";
2
- export type AgentSDKContext = {
3
- /**
4
- * Storage for active tool runs, keyed by tool_use_id.
5
- * Used to correlate PreToolUse and PostToolUse hooks.
6
- */
7
- activeToolRuns: Map<string, {
8
- run: RunTree;
9
- startTime: number;
10
- }>;
11
- /**
12
- * Storage for client-managed runs (subagent sessions and their child tools).
13
- * These are created when processing AssistantMessage content blocks and
14
- * closed when PostToolUse hook fires. Keyed by tool_use_id.
15
- */
16
- clientManagedRuns: Map<string, RunTree>;
17
- /**
18
- * Storage for subagent sessions, keyed by the Task tool's tool_use_id.
19
- * Used to parent LLM turns and tools to the correct subagent.
20
- */
21
- subagentSessions: Map<string, RunTree>;
22
- /**
23
- * Tracks the currently active subagent context (tool_use_id).
24
- * Set when a Task tool is called, cleared when the tool result returns.
25
- * Assistant messages that arrive while a subagent is active belong to that subagent.
26
- */
27
- activeSubagentToolUseId: string | undefined;
28
- /**
29
- * Reference to the current parent run tree for tool tracing.
30
- * Set when a traced query starts, cleared when it ends.
31
- */
32
- currentParentRun: RunTree | undefined;
33
- };
34
- /**
35
- * Configuration options for wrapping Claude Agent SDK with LangSmith tracing.
36
- */
37
- export type WrapClaudeAgentSDKConfig = Partial<Omit<RunTreeConfig, "inputs" | "outputs" | "run_type" | "child_runs" | "parent_run" | "error">>;
1
+ export type SDKAssistantMessage = {
2
+ type: "assistant";
3
+ message: {
4
+ id: string;
5
+ role?: string;
6
+ content: Record<string, any>[];
7
+ usage?: Record<string, any>;
8
+ model?: string;
9
+ };
10
+ parent_tool_use_id: string | null;
11
+ };
12
+ export type SDKSystemMessage = {
13
+ type: "system";
14
+ };
15
+ export type SDKUserMessage = {
16
+ type: "user";
17
+ message: {
18
+ role?: string;
19
+ content: Record<string, any> | Record<string, any>[] | string;
20
+ usage?: Record<string, any>;
21
+ model?: string;
22
+ };
23
+ session_id: string;
24
+ tool_use_result?: unknown;
25
+ parent_tool_use_id: string | null;
26
+ };
27
+ export type SDKResultMessage = {
28
+ type: "result";
29
+ modelUsage: ModelUsage;
30
+ total_cost_usd: number | null;
31
+ is_error: boolean | null;
32
+ num_turns: number | null;
33
+ session_id: string | null;
34
+ duration_ms: number | null;
35
+ duration_api_ms: number | null;
36
+ usage: Record<string, any>;
37
+ };
38
+ export type SDKMessage = SDKAssistantMessage | SDKUserMessage | SDKSystemMessage | SDKResultMessage;
39
+ export type ModelUsage = {
40
+ [key: string]: any;
41
+ };
42
+ export type QueryOptions = {
43
+ [key: string]: any;
44
+ };
45
+ export type BetaContentBlock = {
46
+ [key: string]: any;
47
+ };
48
+ export type BetaToolUseBlock = {
49
+ [key: string]: any;
50
+ };
@@ -1 +1,2 @@
1
+ /* eslint-disable @typescript-eslint/no-explicit-any */
1
2
  export {};
@@ -0,0 +1,180 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.aggregateUsageFromModelUsage = aggregateUsageFromModelUsage;
4
+ exports.extractUsageFromMessage = extractUsageFromMessage;
5
+ exports.correctUsageFromResults = correctUsageFromResults;
6
+ const usage_js_1 = require("../../utils/usage.cjs");
7
+ const utils_js_1 = require("./utils.cjs");
8
+ /**
9
+ * Aggregates usage from modelUsage breakdown (includes all models, including hidden ones).
10
+ * This provides accurate totals when multiple models are used.
11
+ * @internal
12
+ */
13
+ function aggregateUsageFromModelUsage(modelUsage) {
14
+ const metrics = {};
15
+ let totalInputTokens = 0;
16
+ let totalOutputTokens = 0;
17
+ let totalCacheReadTokens = 0;
18
+ let totalCacheCreationTokens = 0;
19
+ // Aggregate across all models
20
+ for (const modelStats of Object.values(modelUsage)) {
21
+ totalInputTokens += modelStats.inputTokens || 0;
22
+ totalOutputTokens += modelStats.outputTokens || 0;
23
+ totalCacheReadTokens += modelStats.cacheReadInputTokens || 0;
24
+ totalCacheCreationTokens += modelStats.cacheCreationInputTokens || 0;
25
+ }
26
+ // Build input_token_details if we have cache tokens
27
+ if (totalCacheReadTokens > 0 || totalCacheCreationTokens > 0) {
28
+ metrics.input_token_details = {
29
+ cache_read: totalCacheReadTokens,
30
+ cache_creation: totalCacheCreationTokens,
31
+ };
32
+ }
33
+ // Sum all input tokens (new + cache read + cache creation)
34
+ const totalPromptTokens = totalInputTokens + totalCacheReadTokens + totalCacheCreationTokens;
35
+ metrics.input_tokens = totalPromptTokens;
36
+ metrics.output_tokens = totalOutputTokens;
37
+ metrics.total_tokens = totalPromptTokens + totalOutputTokens;
38
+ return metrics;
39
+ }
40
+ /**
41
+ * Extracts and normalizes usage metrics from a Claude Agent SDK message.
42
+ * @internal
43
+ */
44
+ function extractUsageFromMessage(message) {
45
+ const metrics = {};
46
+ // Assistant messages contain usage in message.message.usage
47
+ // Result messages contain usage in message.usage
48
+ let usage;
49
+ if (message.type === "assistant") {
50
+ usage = message.message?.usage;
51
+ }
52
+ else if (message.type === "result") {
53
+ usage = message.usage;
54
+ }
55
+ if (!usage || typeof usage !== "object") {
56
+ return metrics;
57
+ }
58
+ // Standard token counts - use LangSmith's expected field names
59
+ const inputTokens = (0, utils_js_1.getNumberProperty)(usage, "input_tokens") || 0;
60
+ const outputTokens = (0, utils_js_1.getNumberProperty)(usage, "output_tokens") || 0;
61
+ // Get cache tokens
62
+ const cacheRead = (0, utils_js_1.getNumberProperty)(usage, "cache_read_input_tokens") || 0;
63
+ const cacheCreation = (0, utils_js_1.getNumberProperty)(usage, "cache_creation_input_tokens") || 0;
64
+ // Build input_token_details if we have cache tokens
65
+ if (cacheRead > 0 || cacheCreation > 0) {
66
+ const inputTokenDetails = (0, usage_js_1.convertAnthropicUsageToInputTokenDetails)(usage);
67
+ if (Object.keys(inputTokenDetails).length > 0) {
68
+ metrics.input_token_details = inputTokenDetails;
69
+ }
70
+ }
71
+ // Sum cache tokens into input_tokens total (matching Python's sum_anthropic_tokens)
72
+ const totalInputTokens = inputTokens + cacheRead + cacheCreation;
73
+ metrics.input_tokens = totalInputTokens;
74
+ metrics.output_tokens = outputTokens;
75
+ metrics.total_tokens = totalInputTokens + outputTokens;
76
+ return metrics;
77
+ }
78
+ /**
79
+ * Corrects usage metrics for assistant runs based on the results of the runs.
80
+ * @internal
81
+ */
82
+ function correctUsageFromResults(resultUsages, assistantRuns) {
83
+ const runByModel = assistantRuns.reduce((acc, run) => {
84
+ const modelId = run.extra?.metadata?.ls_model_name;
85
+ if (!modelId)
86
+ return acc;
87
+ acc[modelId] ??= [];
88
+ acc[modelId].push(run);
89
+ return acc;
90
+ }, {});
91
+ const runUsageByModel = assistantRuns.reduce((acc, run) => {
92
+ const modelId = run.extra?.metadata?.ls_model_name;
93
+ if (!modelId)
94
+ return acc;
95
+ const usageMetadata = { ...run.extra?.metadata?.usage_metadata };
96
+ usageMetadata.input_tokens ??= 0;
97
+ usageMetadata.output_tokens ??= 0;
98
+ usageMetadata.total_tokens ??= 0;
99
+ usageMetadata.input_token_details = {
100
+ ...usageMetadata.input_token_details,
101
+ };
102
+ usageMetadata.input_token_details.cache_read ??= 0;
103
+ usageMetadata.input_token_details.ephemeral_5m_input_tokens ??= 0;
104
+ usageMetadata.input_token_details.ephemeral_1h_input_tokens ??= 0;
105
+ acc[modelId] ??= {
106
+ input_tokens: 0,
107
+ output_tokens: 0,
108
+ total_tokens: 0,
109
+ input_token_details: {
110
+ cache_read: 0,
111
+ cache_creation: 0,
112
+ },
113
+ };
114
+ acc[modelId].input_tokens += usageMetadata.input_tokens;
115
+ acc[modelId].output_tokens += usageMetadata.output_tokens;
116
+ acc[modelId].total_tokens += usageMetadata.total_tokens;
117
+ acc[modelId].input_token_details.cache_read +=
118
+ usageMetadata.input_token_details.cache_read;
119
+ acc[modelId].input_token_details.cache_creation +=
120
+ usageMetadata.input_token_details.ephemeral_5m_input_tokens;
121
+ acc[modelId].input_token_details.cache_creation +=
122
+ usageMetadata.input_token_details.ephemeral_1h_input_tokens;
123
+ return acc;
124
+ }, {});
125
+ const resultUsageMap = Object.fromEntries(Object.entries(resultUsages).map(([modelId, usage]) => [
126
+ modelId,
127
+ {
128
+ input_tokens: usage.inputTokens +
129
+ usage.cacheReadInputTokens +
130
+ usage.cacheCreationInputTokens,
131
+ output_tokens: usage.outputTokens,
132
+ total_tokens: usage.inputTokens +
133
+ usage.cacheReadInputTokens +
134
+ usage.cacheCreationInputTokens +
135
+ usage.outputTokens,
136
+ input_token_details: {
137
+ cache_read: usage.cacheReadInputTokens,
138
+ cache_creation: usage.cacheCreationInputTokens,
139
+ },
140
+ },
141
+ ]));
142
+ for (const modelId in resultUsageMap) {
143
+ const lastRun = runByModel[modelId]?.at(-1);
144
+ const runsUsage = runUsageByModel[modelId];
145
+ const resultUsage = resultUsageMap[modelId];
146
+ if (!runsUsage || !lastRun)
147
+ continue;
148
+ const difference = {
149
+ input_tokens: Math.max(0, resultUsage.input_tokens - runsUsage.input_tokens),
150
+ output_tokens: Math.max(0, resultUsage.output_tokens - runsUsage.output_tokens),
151
+ total_tokens: Math.max(0, resultUsage.total_tokens - runsUsage.total_tokens),
152
+ cache_read: Math.max(0, resultUsage.input_token_details.cache_read -
153
+ runsUsage.input_token_details.cache_read),
154
+ cache_creation: Math.max(0, resultUsage.input_token_details.cache_creation -
155
+ runsUsage.input_token_details.cache_creation),
156
+ };
157
+ if (Object.values(difference).some((value) => value > 0)) {
158
+ // apply difference to the last run
159
+ lastRun.extra ??= {};
160
+ lastRun.extra.metadata ??= {};
161
+ lastRun.extra.metadata.usage_metadata ??= {};
162
+ lastRun.extra.metadata.usage_metadata.input_tokens ??= 0;
163
+ lastRun.extra.metadata.usage_metadata.input_tokens +=
164
+ difference.input_tokens;
165
+ lastRun.extra.metadata.usage_metadata.output_tokens ??= 0;
166
+ lastRun.extra.metadata.usage_metadata.output_tokens +=
167
+ difference.output_tokens;
168
+ lastRun.extra.metadata.usage_metadata.total_tokens ??= 0;
169
+ lastRun.extra.metadata.usage_metadata.total_tokens +=
170
+ difference.total_tokens;
171
+ lastRun.extra.metadata.usage_metadata.input_token_details ??= {};
172
+ lastRun.extra.metadata.usage_metadata.input_token_details.cache_read ??= 0;
173
+ lastRun.extra.metadata.usage_metadata.input_token_details.cache_read +=
174
+ difference.cache_read;
175
+ lastRun.extra.metadata.usage_metadata.input_token_details.ephemeral_5m_input_tokens ??= 0;
176
+ lastRun.extra.metadata.usage_metadata.input_token_details.ephemeral_5m_input_tokens +=
177
+ difference.cache_creation;
178
+ }
179
+ }
180
+ }
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,175 @@
1
+ import { convertAnthropicUsageToInputTokenDetails } from "../../utils/usage.js";
2
+ import { getNumberProperty } from "./utils.js";
3
+ /**
4
+ * Aggregates usage from modelUsage breakdown (includes all models, including hidden ones).
5
+ * This provides accurate totals when multiple models are used.
6
+ * @internal
7
+ */
8
+ export function aggregateUsageFromModelUsage(modelUsage) {
9
+ const metrics = {};
10
+ let totalInputTokens = 0;
11
+ let totalOutputTokens = 0;
12
+ let totalCacheReadTokens = 0;
13
+ let totalCacheCreationTokens = 0;
14
+ // Aggregate across all models
15
+ for (const modelStats of Object.values(modelUsage)) {
16
+ totalInputTokens += modelStats.inputTokens || 0;
17
+ totalOutputTokens += modelStats.outputTokens || 0;
18
+ totalCacheReadTokens += modelStats.cacheReadInputTokens || 0;
19
+ totalCacheCreationTokens += modelStats.cacheCreationInputTokens || 0;
20
+ }
21
+ // Build input_token_details if we have cache tokens
22
+ if (totalCacheReadTokens > 0 || totalCacheCreationTokens > 0) {
23
+ metrics.input_token_details = {
24
+ cache_read: totalCacheReadTokens,
25
+ cache_creation: totalCacheCreationTokens,
26
+ };
27
+ }
28
+ // Sum all input tokens (new + cache read + cache creation)
29
+ const totalPromptTokens = totalInputTokens + totalCacheReadTokens + totalCacheCreationTokens;
30
+ metrics.input_tokens = totalPromptTokens;
31
+ metrics.output_tokens = totalOutputTokens;
32
+ metrics.total_tokens = totalPromptTokens + totalOutputTokens;
33
+ return metrics;
34
+ }
35
+ /**
36
+ * Extracts and normalizes usage metrics from a Claude Agent SDK message.
37
+ * @internal
38
+ */
39
+ export function extractUsageFromMessage(message) {
40
+ const metrics = {};
41
+ // Assistant messages contain usage in message.message.usage
42
+ // Result messages contain usage in message.usage
43
+ let usage;
44
+ if (message.type === "assistant") {
45
+ usage = message.message?.usage;
46
+ }
47
+ else if (message.type === "result") {
48
+ usage = message.usage;
49
+ }
50
+ if (!usage || typeof usage !== "object") {
51
+ return metrics;
52
+ }
53
+ // Standard token counts - use LangSmith's expected field names
54
+ const inputTokens = getNumberProperty(usage, "input_tokens") || 0;
55
+ const outputTokens = getNumberProperty(usage, "output_tokens") || 0;
56
+ // Get cache tokens
57
+ const cacheRead = getNumberProperty(usage, "cache_read_input_tokens") || 0;
58
+ const cacheCreation = getNumberProperty(usage, "cache_creation_input_tokens") || 0;
59
+ // Build input_token_details if we have cache tokens
60
+ if (cacheRead > 0 || cacheCreation > 0) {
61
+ const inputTokenDetails = convertAnthropicUsageToInputTokenDetails(usage);
62
+ if (Object.keys(inputTokenDetails).length > 0) {
63
+ metrics.input_token_details = inputTokenDetails;
64
+ }
65
+ }
66
+ // Sum cache tokens into input_tokens total (matching Python's sum_anthropic_tokens)
67
+ const totalInputTokens = inputTokens + cacheRead + cacheCreation;
68
+ metrics.input_tokens = totalInputTokens;
69
+ metrics.output_tokens = outputTokens;
70
+ metrics.total_tokens = totalInputTokens + outputTokens;
71
+ return metrics;
72
+ }
73
+ /**
74
+ * Corrects usage metrics for assistant runs based on the results of the runs.
75
+ * @internal
76
+ */
77
+ export function correctUsageFromResults(resultUsages, assistantRuns) {
78
+ const runByModel = assistantRuns.reduce((acc, run) => {
79
+ const modelId = run.extra?.metadata?.ls_model_name;
80
+ if (!modelId)
81
+ return acc;
82
+ acc[modelId] ??= [];
83
+ acc[modelId].push(run);
84
+ return acc;
85
+ }, {});
86
+ const runUsageByModel = assistantRuns.reduce((acc, run) => {
87
+ const modelId = run.extra?.metadata?.ls_model_name;
88
+ if (!modelId)
89
+ return acc;
90
+ const usageMetadata = { ...run.extra?.metadata?.usage_metadata };
91
+ usageMetadata.input_tokens ??= 0;
92
+ usageMetadata.output_tokens ??= 0;
93
+ usageMetadata.total_tokens ??= 0;
94
+ usageMetadata.input_token_details = {
95
+ ...usageMetadata.input_token_details,
96
+ };
97
+ usageMetadata.input_token_details.cache_read ??= 0;
98
+ usageMetadata.input_token_details.ephemeral_5m_input_tokens ??= 0;
99
+ usageMetadata.input_token_details.ephemeral_1h_input_tokens ??= 0;
100
+ acc[modelId] ??= {
101
+ input_tokens: 0,
102
+ output_tokens: 0,
103
+ total_tokens: 0,
104
+ input_token_details: {
105
+ cache_read: 0,
106
+ cache_creation: 0,
107
+ },
108
+ };
109
+ acc[modelId].input_tokens += usageMetadata.input_tokens;
110
+ acc[modelId].output_tokens += usageMetadata.output_tokens;
111
+ acc[modelId].total_tokens += usageMetadata.total_tokens;
112
+ acc[modelId].input_token_details.cache_read +=
113
+ usageMetadata.input_token_details.cache_read;
114
+ acc[modelId].input_token_details.cache_creation +=
115
+ usageMetadata.input_token_details.ephemeral_5m_input_tokens;
116
+ acc[modelId].input_token_details.cache_creation +=
117
+ usageMetadata.input_token_details.ephemeral_1h_input_tokens;
118
+ return acc;
119
+ }, {});
120
+ const resultUsageMap = Object.fromEntries(Object.entries(resultUsages).map(([modelId, usage]) => [
121
+ modelId,
122
+ {
123
+ input_tokens: usage.inputTokens +
124
+ usage.cacheReadInputTokens +
125
+ usage.cacheCreationInputTokens,
126
+ output_tokens: usage.outputTokens,
127
+ total_tokens: usage.inputTokens +
128
+ usage.cacheReadInputTokens +
129
+ usage.cacheCreationInputTokens +
130
+ usage.outputTokens,
131
+ input_token_details: {
132
+ cache_read: usage.cacheReadInputTokens,
133
+ cache_creation: usage.cacheCreationInputTokens,
134
+ },
135
+ },
136
+ ]));
137
+ for (const modelId in resultUsageMap) {
138
+ const lastRun = runByModel[modelId]?.at(-1);
139
+ const runsUsage = runUsageByModel[modelId];
140
+ const resultUsage = resultUsageMap[modelId];
141
+ if (!runsUsage || !lastRun)
142
+ continue;
143
+ const difference = {
144
+ input_tokens: Math.max(0, resultUsage.input_tokens - runsUsage.input_tokens),
145
+ output_tokens: Math.max(0, resultUsage.output_tokens - runsUsage.output_tokens),
146
+ total_tokens: Math.max(0, resultUsage.total_tokens - runsUsage.total_tokens),
147
+ cache_read: Math.max(0, resultUsage.input_token_details.cache_read -
148
+ runsUsage.input_token_details.cache_read),
149
+ cache_creation: Math.max(0, resultUsage.input_token_details.cache_creation -
150
+ runsUsage.input_token_details.cache_creation),
151
+ };
152
+ if (Object.values(difference).some((value) => value > 0)) {
153
+ // apply difference to the last run
154
+ lastRun.extra ??= {};
155
+ lastRun.extra.metadata ??= {};
156
+ lastRun.extra.metadata.usage_metadata ??= {};
157
+ lastRun.extra.metadata.usage_metadata.input_tokens ??= 0;
158
+ lastRun.extra.metadata.usage_metadata.input_tokens +=
159
+ difference.input_tokens;
160
+ lastRun.extra.metadata.usage_metadata.output_tokens ??= 0;
161
+ lastRun.extra.metadata.usage_metadata.output_tokens +=
162
+ difference.output_tokens;
163
+ lastRun.extra.metadata.usage_metadata.total_tokens ??= 0;
164
+ lastRun.extra.metadata.usage_metadata.total_tokens +=
165
+ difference.total_tokens;
166
+ lastRun.extra.metadata.usage_metadata.input_token_details ??= {};
167
+ lastRun.extra.metadata.usage_metadata.input_token_details.cache_read ??= 0;
168
+ lastRun.extra.metadata.usage_metadata.input_token_details.cache_read +=
169
+ difference.cache_read;
170
+ lastRun.extra.metadata.usage_metadata.input_token_details.ephemeral_5m_input_tokens ??= 0;
171
+ lastRun.extra.metadata.usage_metadata.input_token_details.ephemeral_5m_input_tokens +=
172
+ difference.cache_creation;
173
+ }
174
+ }
175
+ }