From e1ecfa5200333b0967b98be0c23ce7bd73bf3e54 Mon Sep 17 00:00:00 2001 From: Mike Harrison Date: Mon, 22 Jun 2026 12:32:50 -0400 Subject: [PATCH] fix(diagnostics-otel): keep full model id on spans instead of collapsing to "unknown" (#89981) * fix(diagnostics-otel): keep full model id on spans (was collapsing to "unknown") * test(diagnostics-otel): cover slash model span attribution --------- Co-authored-by: Vincent Koc --- extensions/diagnostics-otel/src/service.test.ts | 14 ++++++++------ extensions/diagnostics-otel/src/service.ts | 7 ++++++- 2 files changed, 14 insertions(+), 7 deletions(-) diff --git a/extensions/diagnostics-otel/src/service.test.ts b/extensions/diagnostics-otel/src/service.test.ts index dce7fb60fb96..60c57da31ac4 100644 --- a/extensions/diagnostics-otel/src/service.test.ts +++ b/extensions/diagnostics-otel/src/service.test.ts @@ -2120,7 +2120,7 @@ describe("diagnostics-otel service", () => { sessionKey: "session-key", sessionId: "session-id", provider: "anthropic", - model: "claude-sonnet-4.6", + model: "anthropic/claude-sonnet-4.6", usage: { input: 100, output: 40, @@ -2136,7 +2136,9 @@ describe("diagnostics-otel service", () => { const modelUsageOptions = startedSpanOptions("openclaw.model.usage"); expect(modelUsageOptions?.attributes?.["gen_ai.operation.name"]).toBe("chat"); expect(modelUsageOptions?.attributes?.["gen_ai.system"]).toBe("anthropic"); - expect(modelUsageOptions?.attributes?.["gen_ai.request.model"]).toBe("claude-sonnet-4.6"); + expect(modelUsageOptions?.attributes?.["gen_ai.request.model"]).toBe( + "anthropic/claude-sonnet-4.6", + ); expect(modelUsageOptions?.attributes?.["gen_ai.usage.input_tokens"]).toBe(150); expect(modelUsageOptions?.attributes?.["gen_ai.usage.output_tokens"]).toBe(40); expect(modelUsageOptions?.attributes?.["gen_ai.usage.cache_read.input_tokens"]).toBe(30); @@ -2163,8 +2165,8 @@ describe("diagnostics-otel service", () => { runId: "run-1", callId: "call-1", sessionKey: "session-key", - provider: "openai", - model: "gpt-5.4", + provider: "anthropic", + model: "anthropic/claude-sonnet-4.6", api: "openai-completions", durationMs: 250, }); @@ -2193,8 +2195,8 @@ describe("diagnostics-otel service", () => { expect(genAiOperationDuration?.record).toHaveBeenCalledTimes(2); expect(genAiOperationDuration?.record).toHaveBeenCalledWith(0.25, { "gen_ai.operation.name": "text_completion", - "gen_ai.provider.name": "openai", - "gen_ai.request.model": "gpt-5.4", + "gen_ai.provider.name": "anthropic", + "gen_ai.request.model": "unknown", }); expect(genAiOperationDuration?.record).toHaveBeenCalledWith(1.25, { "gen_ai.operation.name": "generate_content", diff --git a/extensions/diagnostics-otel/src/service.ts b/extensions/diagnostics-otel/src/service.ts index af8892090588..f2d47940843c 100644 --- a/extensions/diagnostics-otel/src/service.ts +++ b/extensions/diagnostics-otel/src/service.ts @@ -468,7 +468,12 @@ function assignGenAiSpanIdentityAttrs( attrs["gen_ai.system"] = lowCardinalityAttr(input.provider); } if (input.model) { - attrs["gen_ai.request.model"] = lowCardinalityAttr(input.model); + // Span attributes carry the full model id; only metric labels need bounded cardinality + // (the gen_ai metrics below still use lowCardinalityAttr). The low-cardinality allowlist + // regex rejects "/", so provider-qualified ids like "anthropic/claude-sonnet-4.6" collapse + // to "unknown" on the SPAN — breaking model attribution in trace backends (e.g. Langfuse + // reads gen_ai.request.model). Keep the redacted raw model on the span. + attrs["gen_ai.request.model"] = redactSensitiveText(input.model.trim()); } attrs["gen_ai.operation.name"] = genAiOperationName(input.api); }