From 44648440a5211428329315d95e3880568af45ec6 Mon Sep 17 00:00:00 2001 From: Vincent Koc Date: Sat, 25 Apr 2026 12:22:32 -0700 Subject: [PATCH] fix(diagnostics-otel): stabilize genai token metric model attr --- CHANGELOG.md | 1 + .../diagnostics-otel/src/service.test.ts | 24 +++++++++++++++++++ extensions/diagnostics-otel/src/service.ts | 2 +- 3 files changed, 26 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index ac2f927a9aa..ce5c66f9f54 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -42,6 +42,7 @@ Docs: https://docs.openclaw.ai - Diagnostics/OTEL: add GenAI usage token attributes to model-usage spans, including cache read/write input token counts without session identifiers or prompt/response content. Thanks @vincentkoc. - Diagnostics/OTEL: include bounded GenAI operation, provider, and request-model attributes on model-usage spans so token usage remains self-describing without diagnostic identifiers. Thanks @vincentkoc. - Diagnostics/OTEL: keep model-usage span GenAI provider attributes aligned with the existing semantic-convention opt-in policy, using legacy `gen_ai.system` unless latest experimental GenAI conventions are enabled. Thanks @vincentkoc. +- Diagnostics/OTEL: keep `gen_ai.request.model` present on GenAI token usage metrics with a bounded `unknown` fallback when model usage events do not include a model. Thanks @vincentkoc. - Diagnostics/OTEL: add bounded outbound message delivery lifecycle diagnostics and export them as low-cardinality delivery spans/metrics without message body, recipient, room, or media-path data. (#71471) Thanks @vincentkoc and @jlapenna. - Diagnostics/OTEL: emit bounded exec-process diagnostics and export them as `openclaw.exec` spans without exposing command text, working directories, or container identifiers. (#71451) Thanks @vincentkoc and @jlapenna. - Diagnostics/OTEL: support `OPENCLAW_OTEL_PRELOADED=1` so the plugin can reuse an already-registered OpenTelemetry SDK while keeping OpenClaw diagnostic listeners wired. (#71450) Thanks @vincentkoc and @jlapenna. diff --git a/extensions/diagnostics-otel/src/service.test.ts b/extensions/diagnostics-otel/src/service.test.ts index d2053e4cf55..aed978cf933 100644 --- a/extensions/diagnostics-otel/src/service.test.ts +++ b/extensions/diagnostics-otel/src/service.test.ts @@ -740,6 +740,30 @@ describe("diagnostics-otel service", () => { await service.stop?.(ctx); }); + test("keeps GenAI token usage metric model attribute present when model is unavailable", async () => { + const service = createDiagnosticsOtelService(); + const ctx = createOtelContext(OTEL_TEST_ENDPOINT, { metrics: true }); + await service.start(ctx); + + emitDiagnosticEvent({ + type: "model.usage", + provider: "openai", + usage: { input: 2 }, + }); + await flushDiagnosticEvents(); + + expect(telemetryState.histograms.get("gen_ai.client.token.usage")?.record).toHaveBeenCalledWith( + 2, + { + "gen_ai.operation.name": "chat", + "gen_ai.provider.name": "openai", + "gen_ai.request.model": "unknown", + "gen_ai.token.type": "input", + }, + ); + await service.stop?.(ctx); + }); + test("exports GenAI usage attributes on model usage spans without diagnostic identifiers", async () => { const service = createDiagnosticsOtelService(); const ctx = createOtelContext(OTEL_TEST_ENDPOINT, { traces: true }); diff --git a/extensions/diagnostics-otel/src/service.ts b/extensions/diagnostics-otel/src/service.ts index e9f0c03a9ed..6176956b064 100644 --- a/extensions/diagnostics-otel/src/service.ts +++ b/extensions/diagnostics-otel/src/service.ts @@ -904,7 +904,7 @@ export function createDiagnosticsOtelService(): OpenClawPluginService { const genAiAttrs: Record = { "gen_ai.operation.name": "chat", "gen_ai.provider.name": lowCardinalityAttr(evt.provider), - ...(evt.model ? { "gen_ai.request.model": lowCardinalityAttr(evt.model) } : {}), + "gen_ai.request.model": lowCardinalityAttr(evt.model), }; const usage = evt.usage;