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 <vincentkoc@ieee.org>
This commit is contained in:
Mike Harrison
2026-06-22 12:32:50 -04:00
committed by GitHub
parent 7f6a93eb8e
commit e1ecfa5200
2 changed files with 14 additions and 7 deletions

View File

@@ -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",

View File

@@ -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);
}