Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions .changeset/tidy-pandas-greet.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"eve": patch
---

Splits token usage on local trace spans: model and step spans now record `gen_ai.usage.cache_read.input_tokens` and `gen_ai.usage.cache_creation.input_tokens` (OTel GenAI semantic-convention names) alongside the input/output totals when the provider reports detailed usage — cached tokens price differently, so the split makes cost attribution exact. Providers without detailed usage emit only the totals.
2 changes: 2 additions & 0 deletions docs/guides/instrumentation.md
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,8 @@ Use `eve trace ls` to list captured traces and `eve trace <trace>` to inspect a

When a model call is served by Vercel AI Gateway, its `agent.step` span also carries the cost the gateway reported: `gen_ai.usage.cost` (raw inference, USD), `gen_ai.usage.gateway_cost` (with the gateway surcharge), `gen_ai.usage.input_cost` / `gen_ai.usage.output_cost` (the split), and `gen_ai.generation.id` for reconciliation with the gateway dashboard. These attributes only exist for gateway-served calls — other providers emit nothing. Cost per turn is the sum across the turn's step spans.

Model and step spans also split token usage when the provider reports details: `gen_ai.usage.cache_read.input_tokens` and `gen_ai.usage.cache_creation.input_tokens` (named for the [OTel GenAI semantic conventions](https://github.com/open-telemetry/semantic-conventions-genai)) alongside the `agent.usage.input_tokens` / `agent.usage.output_tokens` totals — cached tokens price differently, so the split makes cost attribution exact. Providers without detailed usage emit only the totals.

The local writer is an internal development default, not a second provider layered over authored instrumentation. When `instrumentation.ts` exists, its setup retains control and the zero-config writer is not installed.

### Local trace retention
Expand Down
8 changes: 7 additions & 1 deletion packages/eve/src/harness/agent-otel-provider.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -86,7 +86,11 @@ async function emitAttempt(input: {
finishReason: "tool-calls",
performance: { responseTimeMs: 10 },
responseId: "response-1",
usage: { inputTokens: 10, outputTokens: 5 },
usage: {
inputTokenDetails: { cacheReadTokens: 4, cacheWriteTokens: 2 },
inputTokens: 10,
outputTokens: 5,
},
},
]);
await Reflect.apply(bridge.onToolExecutionStart!, bridge, [
Expand Down Expand Up @@ -202,6 +206,8 @@ describe("createAgentOtelInstrumentation", () => {
"agent.root.session.id": "session-1",
"agent.usage.input_tokens": 10,
"agent.usage.output_tokens": 5,
"gen_ai.usage.cache_creation.input_tokens": 2,
"gen_ai.usage.cache_read.input_tokens": 4,
});
expect(action.attributes).toMatchObject({
"agent.action.kind": "tool",
Expand Down
19 changes: 18 additions & 1 deletion packages/eve/src/harness/agent-otel-provider.ts
Original file line number Diff line number Diff line change
Expand Up @@ -516,14 +516,31 @@ function modelSpanName(operationName: string): string {

function setUsage(
span: Span,
usage: { readonly inputTokens?: number; readonly outputTokens?: number },
usage: {
readonly inputTokenDetails?: {
readonly cacheReadTokens?: number;
readonly cacheWriteTokens?: number;
};
readonly inputTokens?: number;
readonly outputTokens?: number;
},
): void {
if (usage.inputTokens !== undefined) {
span.setAttribute("agent.usage.input_tokens", usage.inputTokens);
}
if (usage.outputTokens !== undefined) {
span.setAttribute("agent.usage.output_tokens", usage.outputTokens);
}
// Cached tokens price differently from plain input, so keep the split.
// Named for the OTel GenAI semantic conventions; present only when the
// provider reports details — others emit nothing.
const details = usage.inputTokenDetails;
if (details?.cacheReadTokens !== undefined) {
span.setAttribute("gen_ai.usage.cache_read.input_tokens", details.cacheReadTokens);
}
if (details?.cacheWriteTokens !== undefined) {
span.setAttribute("gen_ai.usage.cache_creation.input_tokens", details.cacheWriteTokens);
}
}

function recordError(span: Span, error: unknown): void {
Expand Down
Loading