@sebastiantuyu/agest 0.3.3-next.6 → 0.3.3-next.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.
@@ -48,6 +48,7 @@ export async function createTracingHandle(baselineMs) {
48
48
  const endMs = now() - baselineMs;
49
49
  const tokens = extractTokensFromLLMOutput(output);
50
50
  const providerCost = extractProviderCost(output);
51
+ const cachedInputTokens = extractCachedTokens(output);
51
52
  const name = open.name ?? extractModelNameFromOutput(output) ?? "model";
52
53
  if (name && name !== "model")
53
54
  lastModelName = name;
@@ -64,6 +65,7 @@ export async function createTracingHandle(baselineMs) {
64
65
  endMs,
65
66
  durationMs: Math.max(0, endMs - open.startMs),
66
67
  tokens,
68
+ cachedInputTokens,
67
69
  cost: stripCostIfEmpty(cost),
68
70
  });
69
71
  }
@@ -82,10 +84,10 @@ export async function createTracingHandle(baselineMs) {
82
84
  error: err?.message ?? String(err),
83
85
  });
84
86
  }
85
- handleToolStart(tool, _input, runId) {
87
+ handleToolStart(tool, _input, runId, _parentRunId, _tags, _metadata, runName) {
86
88
  openTools.set(runId, {
87
89
  startMs: now() - baselineMs,
88
- name: extractToolName(tool) ?? "tool",
90
+ name: extractToolName(tool, runName) ?? "tool",
89
91
  });
90
92
  }
91
93
  handleToolEnd(_output, runId) {
@@ -252,30 +254,82 @@ function extractTokensFromLLMOutput(output) {
252
254
  return undefined;
253
255
  return { input, output: out };
254
256
  }
257
+ /** Collect the usage-bearing objects LangChain/OpenRouter may attach to an LLM result. */
258
+ function usageObjects(output) {
259
+ const msg = output?.generations?.[0]?.[0]?.message;
260
+ return [
261
+ output?.llmOutput?.usage,
262
+ output?.llmOutput?.tokenUsage,
263
+ output?.llmOutput?.estimatedTokenUsage,
264
+ output?.llmOutput,
265
+ msg?.usage_metadata,
266
+ msg?.response_metadata?.usage,
267
+ msg?.response_metadata?.tokenUsage,
268
+ msg?.response_metadata?.estimatedTokenUsage,
269
+ msg?.response_metadata,
270
+ msg?.additional_kwargs?.usage,
271
+ ].filter((u) => u && typeof u === "object");
272
+ }
273
+ /**
274
+ * OpenRouter (with `usage: { include: true }`) reports real USD cost. LangChain
275
+ * surfaces it inconsistently across versions, so scan the known usage objects
276
+ * for a numeric `cost` / `total_cost`.
277
+ */
255
278
  function extractProviderCost(output) {
256
- const candidates = [
257
- output?.llmOutput?.usage?.cost,
258
- output?.llmOutput?.cost,
259
- output?.generations?.[0]?.[0]?.message?.usage_metadata?.total_cost,
260
- output?.generations?.[0]?.[0]?.message?.response_metadata?.usage?.cost,
261
- output?.generations?.[0]?.[0]?.message?.response_metadata?.cost,
262
- ];
263
- for (const c of candidates) {
279
+ for (const u of usageObjects(output)) {
280
+ const c = (typeof u.cost === "number" ? u.cost : undefined) ??
281
+ (typeof u.total_cost === "number" ? u.total_cost : undefined) ??
282
+ (typeof u.cost_usd === "number" ? u.cost_usd : undefined) ??
283
+ (typeof u.cost_details?.upstream_inference_cost === "number"
284
+ ? u.cost_details.upstream_inference_cost
285
+ : undefined);
264
286
  if (typeof c === "number" && Number.isFinite(c))
265
287
  return c;
266
288
  }
267
289
  return undefined;
268
290
  }
269
- function extractToolName(tool) {
270
- if (!tool)
271
- return undefined;
272
- if (typeof tool.name === "string")
273
- return tool.name;
274
- if (Array.isArray(tool.id) && tool.id.length > 0) {
275
- return String(tool.id[tool.id.length - 1]);
291
+ /**
292
+ * Cached (prompt-cache hit) input tokens, when the provider reports them.
293
+ * Charged at a fraction of the normal input rate, so surfacing them lets the
294
+ * report explain why provider cost is below the flat-table estimate.
295
+ */
296
+ function extractCachedTokens(output) {
297
+ for (const u of usageObjects(output)) {
298
+ const cached = u.input_token_details?.cache_read ??
299
+ u.prompt_tokens_details?.cached_tokens ??
300
+ u.cache_read_input_tokens ??
301
+ u.cached_tokens;
302
+ if (typeof cached === "number" && cached > 0)
303
+ return cached;
276
304
  }
277
305
  return undefined;
278
306
  }
307
+ const TOOL_CLASS_NAMES = new Set([
308
+ "DynamicStructuredTool",
309
+ "DynamicTool",
310
+ "StructuredTool",
311
+ "Tool",
312
+ ]);
313
+ function extractToolName(tool, runName) {
314
+ // `runName` is the actual tool name LangChain assigns the run (e.g.
315
+ // "search_recipes"); prefer it over the serialized class name.
316
+ if (runName && !TOOL_CLASS_NAMES.has(runName))
317
+ return runName;
318
+ if (tool) {
319
+ if (typeof tool.name === "string" && !TOOL_CLASS_NAMES.has(tool.name))
320
+ return tool.name;
321
+ if (typeof tool.kwargs?.name === "string")
322
+ return tool.kwargs.name;
323
+ if (Array.isArray(tool.id) && tool.id.length > 0) {
324
+ const last = String(tool.id[tool.id.length - 1]);
325
+ if (!TOOL_CLASS_NAMES.has(last))
326
+ return last;
327
+ }
328
+ if (typeof tool.name === "string")
329
+ return tool.name;
330
+ }
331
+ return runName;
332
+ }
279
333
  function stripCostIfEmpty(cost) {
280
334
  if (cost.source === "unavailable" && cost.totalUsd == null)
281
335
  return undefined;
package/dist/preview.js CHANGED
@@ -222,6 +222,7 @@ function renderWaterfallHtml(report) {
222
222
  `${e.kind}: ${e.name}`,
223
223
  `start ${Math.round(e.startMs)}ms · ${Math.round(e.durationMs)}ms`,
224
224
  e.tokens ? `${e.tokens.input}→${e.tokens.output} tok` : "",
225
+ e.cachedInputTokens ? `${e.cachedInputTokens} cached` : "",
225
226
  e.costUsd != null ? fmtUsdHtml(e.costUsd) : "",
226
227
  e.error ? `error: ${e.error}` : "",
227
228
  ]
package/dist/reporter.js CHANGED
@@ -126,6 +126,9 @@ function renderTimelineEvent(e) {
126
126
  if (e.tokens) {
127
127
  out.push(` tokens: { input: ${e.tokens.input}, output: ${e.tokens.output} }`);
128
128
  }
129
+ if (e.cachedInputTokens != null && e.cachedInputTokens > 0) {
130
+ out.push(` cached_input_tokens: ${e.cachedInputTokens}`);
131
+ }
129
132
  if (e.cost?.totalUsd != null) {
130
133
  out.push(` cost_usd: ${formatUsd(e.cost.totalUsd)}`);
131
134
  out.push(` cost_source: ${e.cost.source}`);
package/dist/reports.d.ts CHANGED
@@ -18,6 +18,7 @@ export interface ParsedTimelineEvent {
18
18
  input: number;
19
19
  output: number;
20
20
  };
21
+ cachedInputTokens?: number;
21
22
  costUsd?: number;
22
23
  costSource?: string;
23
24
  runIndex?: number;
package/dist/reports.js CHANGED
@@ -205,6 +205,9 @@ export function parseScenes(content) {
205
205
  case "tokens":
206
206
  event.tokens = parseTokens(value);
207
207
  break;
208
+ case "cached_input_tokens":
209
+ event.cachedInputTokens = parseInt(value, 10);
210
+ break;
208
211
  case "cost_usd":
209
212
  event.costUsd = parseFloat(value);
210
213
  break;
package/dist/types.d.ts CHANGED
@@ -21,6 +21,8 @@ export interface TimelineEvent {
21
21
  input: number;
22
22
  output: number;
23
23
  };
24
+ /** Prompt-cache-hit input tokens (subset of tokens.input), when reported by the provider */
25
+ cachedInputTokens?: number;
24
26
  cost?: CostBreakdown;
25
27
  /** Index of the run this event belongs to (only set when aggregating across multi-run scenes) */
26
28
  runIndex?: number;
package/dist/waterfall.js CHANGED
@@ -39,7 +39,8 @@ export function renderTerminalWaterfall(events, opts = {}) {
39
39
  const nameLabel = truncate(e.name, nameWidth).padEnd(nameWidth);
40
40
  const dur = `${Math.round(e.durationMs)}ms`.padStart(7);
41
41
  const cost = e.cost?.totalUsd != null ? ` ${fmtUsd(e.cost.totalUsd)}` : "";
42
+ const cached = e.cachedInputTokens ? ` ${c.dim(`(${e.cachedInputTokens} cached)`)}` : "";
42
43
  const err = e.error ? ` ${c.red("✗ " + truncate(e.error, 40))}` : "";
43
- return `${indent}${c.dim(kindLabel)} ${nameLabel} ${bar} ${c.dim(dur)}${c.dim(cost)}${err}`;
44
+ return `${indent}${c.dim(kindLabel)} ${nameLabel} ${bar} ${c.dim(dur)}${c.dim(cost)}${cached}${err}`;
44
45
  });
45
46
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@sebastiantuyu/agest",
3
- "version": "0.3.3-next.6",
3
+ "version": "0.3.3-next.7",
4
4
  "description": "A testing library for agents",
5
5
  "repository": {
6
6
  "type": "git",