diff --git a/docs/specs/otel-genai-alignment.md b/docs/specs/otel-genai-alignment.md index 5827bf4ee8ff..c4127dbe6e7a 100644 --- a/docs/specs/otel-genai-alignment.md +++ b/docs/specs/otel-genai-alignment.md @@ -11,7 +11,7 @@ Enrich the existing `diagnostics-otel` plugin to emit spans, metrics, and events that follow the [OTel GenAI semantic conventions](https://opentelemetry.io/docs/specs/semconv/gen-ai/) so that OpenClaw telemetry works out-of-the-box with GenAI-aware backends -(Datadog, Grafana, Langfuse, Arize, etc.) while keeping the existing +(Datadog, Grafana, Orq, LangWatch, Langfuse, Arize, etc.) while keeping the existing `openclaw.*` operational telemetry intact. --- diff --git a/extensions/diagnostics-otel/src/service.test.ts b/extensions/diagnostics-otel/src/service.test.ts index 747999a2879a..54c97a6e6fc6 100644 --- a/extensions/diagnostics-otel/src/service.test.ts +++ b/extensions/diagnostics-otel/src/service.test.ts @@ -299,7 +299,14 @@ describe("diagnostics-otel service", () => { finishReasons: ["stop"], sessionKey: "agent:main:main", sessionId: "sess-001", - usage: { input: 100, output: 50, cacheRead: 80, cacheWrite: 0, total: 230 }, + usage: { + input: 100, + output: 50, + cacheRead: 80, + cacheWrite: 0, + promptTokens: 180, + total: 230, + }, durationMs: 1500, }); @@ -323,6 +330,12 @@ describe("diagnostics-otel service", () => { expect(attrs["openclaw.tokens.input"]).toBe(100); expect(attrs["openclaw.tokens.output"]).toBe(50); expect(attrs["openclaw.tokens.cache_read"]).toBe(80); + expect(attrs["gen_ai.usage.cache_read.input_tokens"]).toBe(80); + expect(attrs["gen_ai.usage.cache_creation.input_tokens"]).toBe(0); + // gen_ai.usage.input_tokens should be promptTokens (input + cacheRead + cacheWrite) + // per OTEL semconv, not raw input which excludes cached tokens + expect(attrs["gen_ai.usage.input_tokens"]).toBe(180); + expect(attrs["gen_ai.usage.output_tokens"]).toBe(50); await service.stop?.(); }); diff --git a/extensions/diagnostics-otel/src/service.ts b/extensions/diagnostics-otel/src/service.ts index 4e5f2c3964bf..d9dfc1e516c8 100644 --- a/extensions/diagnostics-otel/src/service.ts +++ b/extensions/diagnostics-otel/src/service.ts @@ -720,16 +720,26 @@ export function createDiagnosticsOtelService(): OpenClawPluginService { } if (typeof usage.output === "number") { spanAttrs["openclaw.tokens.output"] = usage.output; + spanAttrs["gen_ai.usage.output_tokens"] = usage.output; } if (typeof usage.cacheRead === "number") { spanAttrs["openclaw.tokens.cache_read"] = usage.cacheRead; + spanAttrs["gen_ai.usage.cache_read.input_tokens"] = usage.cacheRead; } if (typeof usage.cacheWrite === "number") { spanAttrs["openclaw.tokens.cache_write"] = usage.cacheWrite; + spanAttrs["gen_ai.usage.cache_creation.input_tokens"] = usage.cacheWrite; } if (typeof usage.total === "number") { spanAttrs["openclaw.tokens.total"] = usage.total; } + // OTEL GenAI semconv: gen_ai.usage.input_tokens SHOULD include all input + // tokens including cached tokens. Use promptTokens (input + cacheRead + + // cacheWrite) when available, fall back to raw input. + const inputTokensForOtel = usage.promptTokens ?? usage.input; + if (typeof inputTokensForOtel === "number") { + spanAttrs["gen_ai.usage.input_tokens"] = inputTokensForOtel; + } if (evt.responseModel) { spanAttrs["gen_ai.response.model"] = evt.responseModel; } @@ -923,7 +933,6 @@ export function createDiagnosticsOtelService(): OpenClawPluginService { }; if (typeof usage.input === "number") { spanAttrs["openclaw.tokens.input"] = usage.input; - spanAttrs["gen_ai.usage.input_tokens"] = usage.input; } if (typeof usage.output === "number") { spanAttrs["openclaw.tokens.output"] = usage.output; @@ -931,13 +940,22 @@ export function createDiagnosticsOtelService(): OpenClawPluginService { } if (typeof usage.cacheRead === "number") { spanAttrs["openclaw.tokens.cache_read"] = usage.cacheRead; + spanAttrs["gen_ai.usage.cache_read.input_tokens"] = usage.cacheRead; } if (typeof usage.cacheWrite === "number") { spanAttrs["openclaw.tokens.cache_write"] = usage.cacheWrite; + spanAttrs["gen_ai.usage.cache_creation.input_tokens"] = usage.cacheWrite; } if (typeof usage.total === "number") { spanAttrs["openclaw.tokens.total"] = usage.total; } + // OTEL GenAI semconv: gen_ai.usage.input_tokens SHOULD include all input + // tokens including cached tokens. Use promptTokens (input + cacheRead + + // cacheWrite) when available, fall back to raw input. + const inputTokensForOtel = usage.promptTokens ?? usage.input; + if (typeof inputTokensForOtel === "number") { + spanAttrs["gen_ai.usage.input_tokens"] = inputTokensForOtel; + } if (evt.responseModel) { spanAttrs["gen_ai.response.model"] = evt.responseModel; }