From ce6ca28100ab65eb6e28ce8842c5d0f641e6bd59 Mon Sep 17 00:00:00 2001 From: Marc Seiler Date: Tue, 21 Apr 2026 18:38:31 -0400 Subject: [PATCH] feat(traces): align spans with OpenInference semantics --- bun.lock | 3 ++ package.json | 1 + src/handlers/message.ts | 102 ++++++++++++++++++++++++++++++----- src/handlers/session.ts | 14 ++++- src/index.ts | 27 ++++++++-- src/types.ts | 2 + tests/handlers/spans.test.ts | 59 ++++++++++++++------ tests/helpers.ts | 2 + 8 files changed, 176 insertions(+), 34 deletions(-) diff --git a/bun.lock b/bun.lock index fed699d..1989cd7 100644 --- a/bun.lock +++ b/bun.lock @@ -5,6 +5,7 @@ "": { "name": "opencode-plugin-otel", "dependencies": { + "@arizeai/openinference-semantic-conventions": "^2.2.0", "@opencode-ai/plugin": "^1.2.23", "@opencode-ai/sdk": "^1.2.23", "@opentelemetry/api": "^1.9.0", @@ -30,6 +31,8 @@ }, }, "packages": { + "@arizeai/openinference-semantic-conventions": ["@arizeai/openinference-semantic-conventions@2.2.0", "", {}, "sha512-jyVeggDSuVFyOXWXqShKsRxHc5RrE7jRN2D0LGMGYDbo4oHeCJCgUezHQITT7t7FPNQxj5O+NcqERhznnDx6UQ=="], + "@es-joy/jsdoccomment": ["@es-joy/jsdoccomment@0.50.2", "", { "dependencies": { "@types/estree": "^1.0.6", "@typescript-eslint/types": "^8.11.0", "comment-parser": "1.4.1", "esquery": "^1.6.0", "jsdoc-type-pratt-parser": "~4.1.0" } }, "sha512-YAdE/IJSpwbOTiaURNCKECdAwqrJuFiZhylmesBcIRawtYKnBR2wxPhoIewMg+Yu+QuYvHfJNReWpoxGBKOChA=="], "@eslint-community/eslint-utils": ["@eslint-community/eslint-utils@4.9.1", "", { "dependencies": { "eslint-visitor-keys": "^3.4.3" }, "peerDependencies": { "eslint": "^6.0.0 || ^7.0.0 || >=8.0.0" } }, "sha512-phrYmNiYppR7znFEdqgfWHXR6NCkZEK7hwWDHZUjit/2/U0r6XvkDl0SYnoM51Hq7FhCGdLDT6zxCCOY1hexsQ=="], diff --git a/package.json b/package.json index 793ba09..1f8c588 100644 --- a/package.json +++ b/package.json @@ -4,6 +4,7 @@ "url": "https://github.com/DEVtheOPS/opencode-plugin-otel/issues" }, "dependencies": { + "@arizeai/openinference-semantic-conventions": "^2.2.0", "@opencode-ai/plugin": "^1.2.23", "@opencode-ai/sdk": "^1.2.23", "@opentelemetry/api": "^1.9.0", diff --git a/src/handlers/message.ts b/src/handlers/message.ts index 13e3d99..cbd6916 100644 --- a/src/handlers/message.ts +++ b/src/handlers/message.ts @@ -1,9 +1,38 @@ import { SeverityNumber } from "@opentelemetry/api-logs" import { SpanStatusCode, SpanKind, context, trace } from "@opentelemetry/api" import type { AssistantMessage, EventMessageUpdated, EventMessagePartUpdated, ToolPart } from "@opencode-ai/sdk" +import { + AGENT_NAME, + INPUT_MIME_TYPE, + INPUT_VALUE, + LLM_COST_TOTAL, + LLM_INPUT_MESSAGES, + LLM_MODEL_NAME, + LLM_OUTPUT_MESSAGES, + LLM_PROVIDER, + LLM_SYSTEM, + LLM_TOKEN_COUNT_COMPLETION, + LLM_TOKEN_COUNT_COMPLETION_DETAILS_REASONING, + LLM_TOKEN_COUNT_PROMPT, + LLM_TOKEN_COUNT_PROMPT_DETAILS_CACHE_READ, + LLM_TOKEN_COUNT_PROMPT_DETAILS_CACHE_WRITE, + LLM_TOKEN_COUNT_TOTAL, + MimeType, + OpenInferenceSpanKind, + OUTPUT_MIME_TYPE, + OUTPUT_VALUE, + SemanticConventions, + SESSION_ID, + TOOL_ID, + TOOL_NAME, + TOOL_PARAMETERS, +} from "@arizeai/openinference-semantic-conventions" import { errorSummary, setBoundedMap, accumulateSessionTotals, isMetricEnabled, isTraceEnabled } from "../util.ts" import type { HandlerContext } from "../types.ts" +const OPENINFERENCE_SPAN_KIND = SemanticConventions.OPENINFERENCE_SPAN_KIND +const LLM_FINISH_REASON = "llm.finish_reason" + type SubtaskPart = { type: "subtask" sessionID: string @@ -79,13 +108,23 @@ export function handleMessageUpdated(e: EventMessageUpdated, ctx: HandlerContext const msgKey = `${sessionID}:${assistant.id}` const msgSpan = ctx.messageSpans.get(msgKey) if (msgSpan) { + const outputText = ctx.messageOutputs.get(msgKey) msgSpan.setAttributes({ - "gen_ai.usage.input_tokens": assistant.tokens.input, - "gen_ai.usage.output_tokens": assistant.tokens.output, - "gen_ai.usage.reasoning_tokens": assistant.tokens.reasoning, - "gen_ai.usage.cache_read_tokens": assistant.tokens.cache.read, - "gen_ai.usage.cache_creation_tokens": assistant.tokens.cache.write, - "gen_ai.response.finish_reason": assistant.error ? "error" : "stop", + [LLM_TOKEN_COUNT_PROMPT]: assistant.tokens.input, + [LLM_TOKEN_COUNT_COMPLETION]: assistant.tokens.output, + [LLM_TOKEN_COUNT_COMPLETION_DETAILS_REASONING]: assistant.tokens.reasoning, + [LLM_TOKEN_COUNT_PROMPT_DETAILS_CACHE_READ]: assistant.tokens.cache.read, + [LLM_TOKEN_COUNT_PROMPT_DETAILS_CACHE_WRITE]: assistant.tokens.cache.write, + [LLM_TOKEN_COUNT_TOTAL]: totalTokens, + [LLM_FINISH_REASON]: assistant.error ? "error" : (assistant.finish ?? "stop"), + [LLM_COST_TOTAL]: assistant.cost, + ...(outputText + ? { + [OUTPUT_VALUE]: outputText, + [OUTPUT_MIME_TYPE]: MimeType.TEXT, + [LLM_OUTPUT_MESSAGES]: JSON.stringify([{ role: "assistant", content: outputText }]), + } + : {}), cost_usd: assistant.cost, duration_ms: duration, }) @@ -96,6 +135,7 @@ export function handleMessageUpdated(e: EventMessageUpdated, ctx: HandlerContext } msgSpan.end(assistant.time.completed) ctx.messageSpans.delete(msgKey) + ctx.messageOutputs.delete(msgKey) } if (assistant.error) { @@ -171,6 +211,12 @@ export function handleMessageUpdated(e: EventMessageUpdated, ctx: HandlerContext export function handleMessagePartUpdated(e: EventMessagePartUpdated, ctx: HandlerContext) { const part = e.properties.part + if (part.type === "text") { + const key = `${part.sessionID}:${part.messageID}` + ctx.messageOutputs.set(key, `${ctx.messageOutputs.get(key) ?? ""}${part.text}`) + return + } + if (part.type === "subtask") { const subtask = part as unknown as SubtaskPart if (isMetricEnabled("subtask.count", ctx)) { @@ -219,8 +265,13 @@ export function handleMessagePartUpdated(e: EventMessagePartUpdated, ctx: Handle startTime: toolPart.state.time.start, kind: SpanKind.INTERNAL, attributes: { - "session.id": toolPart.sessionID, - "tool.name": toolPart.tool, + [OPENINFERENCE_SPAN_KIND]: OpenInferenceSpanKind.TOOL, + [SESSION_ID]: toolPart.sessionID, + [TOOL_ID]: toolPart.callID, + [TOOL_NAME]: toolPart.tool, + [TOOL_PARAMETERS]: JSON.stringify(toolPart.state.input), + [INPUT_VALUE]: JSON.stringify(toolPart.state.input), + [INPUT_MIME_TYPE]: MimeType.JSON, ...ctx.commonAttrs, }, }, @@ -269,8 +320,13 @@ export function handleMessagePartUpdated(e: EventMessagePartUpdated, ctx: Handle startTime: start, kind: SpanKind.INTERNAL, attributes: { - "session.id": toolPart.sessionID, - "tool.name": toolPart.tool, + [OPENINFERENCE_SPAN_KIND]: OpenInferenceSpanKind.TOOL, + [SESSION_ID]: toolPart.sessionID, + [TOOL_ID]: toolPart.callID, + [TOOL_NAME]: toolPart.tool, + [TOOL_PARAMETERS]: JSON.stringify(toolPart.state.input), + [INPUT_VALUE]: JSON.stringify(toolPart.state.input), + [INPUT_MIME_TYPE]: MimeType.JSON, ...ctx.commonAttrs, }, }, @@ -280,10 +336,18 @@ export function handleMessagePartUpdated(e: EventMessagePartUpdated, ctx: Handle toolSpan.setAttribute("tool.success", success) if (success) { const output = (toolPart.state as { output: string }).output + toolSpan.setAttributes({ + [OUTPUT_VALUE]: output, + [OUTPUT_MIME_TYPE]: MimeType.TEXT, + }) toolSpan.setAttribute("tool.result_size_bytes", Buffer.byteLength(output, "utf8")) toolSpan.setStatus({ code: SpanStatusCode.OK }) } else { const err = (toolPart.state as { error: string }).error + toolSpan.setAttributes({ + [OUTPUT_VALUE]: err, + [OUTPUT_MIME_TYPE]: MimeType.TEXT, + }) toolSpan.setAttribute("tool.error", err) toolSpan.setStatus({ code: SpanStatusCode.ERROR, message: err }) } @@ -349,14 +413,24 @@ export function startMessageSpan( : context.active() const msgSpan = ctx.tracer.startSpan( - "gen_ai.chat", + `${ctx.tracePrefix}llm`, { startTime, kind: SpanKind.CLIENT, attributes: { - "gen_ai.system": providerID, - "gen_ai.request.model": modelID, - "session.id": sessionID, + [OPENINFERENCE_SPAN_KIND]: OpenInferenceSpanKind.LLM, + [SESSION_ID]: sessionID, + [AGENT_NAME]: ctx.sessionTotals.get(sessionID)?.agent ?? "unknown", + [LLM_SYSTEM]: providerID, + [LLM_PROVIDER]: providerID, + [LLM_MODEL_NAME]: modelID, + ...(ctx.sessionInputs.has(sessionID) + ? { + [INPUT_VALUE]: ctx.sessionInputs.get(sessionID)!, + [INPUT_MIME_TYPE]: MimeType.TEXT, + [LLM_INPUT_MESSAGES]: JSON.stringify([{ role: "user", content: ctx.sessionInputs.get(sessionID)! }]), + } + : {}), ...ctx.commonAttrs, }, }, diff --git a/src/handlers/session.ts b/src/handlers/session.ts index 393e631..d5409c4 100644 --- a/src/handlers/session.ts +++ b/src/handlers/session.ts @@ -1,9 +1,12 @@ import { SeverityNumber } from "@opentelemetry/api-logs" import { SpanStatusCode, context, trace } from "@opentelemetry/api" import type { EventSessionCreated, EventSessionIdle, EventSessionError, EventSessionStatus } from "@opencode-ai/sdk" +import { AGENT_NAME, OpenInferenceSpanKind, SemanticConventions, SESSION_ID } from "@arizeai/openinference-semantic-conventions" import { errorSummary, setBoundedMap, isMetricEnabled, isTraceEnabled } from "../util.ts" import type { HandlerContext } from "../types.ts" +const OPENINFERENCE_SPAN_KIND = SemanticConventions.OPENINFERENCE_SPAN_KIND + /** Increments the session counter, records start time, starts the root session span, and emits a `session.created` log event. */ export function handleSessionCreated(e: EventSessionCreated, ctx: HandlerContext) { const { id: sessionID, time, parentID } = e.properties.info @@ -29,7 +32,9 @@ export function handleSessionCreated(e: EventSessionCreated, ctx: HandlerContext { startTime: createdAt, attributes: { - "session.id": sessionID, + [OPENINFERENCE_SPAN_KIND]: OpenInferenceSpanKind.AGENT, + [SESSION_ID]: sessionID, + [AGENT_NAME]: "unknown", "session.is_subagent": isSubagent, ...ctx.commonAttrs, }, @@ -61,6 +66,7 @@ function sweepSession(sessionID: string, ctx: HandlerContext) { ctx.pendingToolSpans.delete(key) } } + ctx.sessionInputs.delete(sessionID) const msgPrefix = `${sessionID}:` for (const [key, span] of ctx.messageSpans) { if (key.startsWith(msgPrefix)) { @@ -69,6 +75,9 @@ function sweepSession(sessionID: string, ctx: HandlerContext) { ctx.messageSpans.delete(key) } } + for (const key of ctx.messageOutputs.keys()) { + if (key.startsWith(msgPrefix)) ctx.messageOutputs.delete(key) + } } /** Emits a `session.idle` log event, records duration and session total histograms, ends the session span, and clears pending state. */ @@ -98,6 +107,7 @@ export function handleSessionIdle(e: EventSessionIdle, ctx: HandlerContext) { if (sessionSpan) { if (totals) { sessionSpan.setAttributes({ + [AGENT_NAME]: totals.agent, "session.total_tokens": totals.tokens, "session.total_cost_usd": totals.cost, "session.total_messages": totals.messages, @@ -140,6 +150,8 @@ export function handleSessionError(e: EventSessionError, ctx: HandlerContext) { if (rawID) { const sessionSpan = ctx.sessionSpans.get(rawID) if (sessionSpan) { + const totals = ctx.sessionTotals.get(rawID) + if (totals) sessionSpan.setAttribute(AGENT_NAME, totals.agent) sessionSpan.setStatus({ code: SpanStatusCode.ERROR, message: error }) sessionSpan.setAttribute("error", error) sessionSpan.end() diff --git a/src/index.ts b/src/index.ts index 225842d..560f324 100644 --- a/src/index.ts +++ b/src/index.ts @@ -2,6 +2,7 @@ import type { Plugin } from "@opencode-ai/plugin" import { SeverityNumber } from "@opentelemetry/api-logs" import { logs } from "@opentelemetry/api-logs" import { trace } from "@opentelemetry/api" +import { AGENT_NAME } from "@arizeai/openinference-semantic-conventions" import pkg from "../package.json" with { type: "json" } import type { EventSessionCreated, @@ -86,6 +87,8 @@ export const OtelPlugin: Plugin = async ({ project, client }) => { const sessionTotals = new Map() const sessionSpans = new Map() const messageSpans = new Map() + const sessionInputs = new Map() + const messageOutputs = new Map() const { disabledMetrics, disabledTraces } = config const commonAttrs = { "project.id": project.id } as const @@ -111,6 +114,8 @@ export const OtelPlugin: Plugin = async ({ project, client }) => { tracePrefix: config.metricPrefix, sessionSpans, messageSpans, + sessionInputs, + messageOutputs, } async function shutdown() { @@ -153,10 +158,24 @@ export const OtelPlugin: Plugin = async ({ project, client }) => { const agent = input.agent ?? "unknown" const totals = sessionTotals.get(input.sessionID) if (totals) totals.agent = agent - const promptLength = output.parts.reduce( - (acc, p) => (p.type === "text" ? acc + p.text.length : acc), - 0, - ) + const sessionSpan = sessionSpans.get(input.sessionID) + if (sessionSpan) sessionSpan.setAttribute(AGENT_NAME, agent) + const promptText = output.parts.map((part) => { + switch (part.type) { + case "text": + return part.text + case "file": + return part.filename ?? part.url + case "agent": + return part.name + case "subtask": + return part.description + default: + return "" + } + }).filter(Boolean).join("\n") + sessionInputs.set(input.sessionID, promptText) + const promptLength = promptText.length logger.emit({ severityNumber: SeverityNumber.INFO, severityText: "INFO", diff --git a/src/types.ts b/src/types.ts index 8d21c3e..0132998 100644 --- a/src/types.ts +++ b/src/types.ts @@ -77,4 +77,6 @@ export type HandlerContext = { tracePrefix: string sessionSpans: Map messageSpans: Map + sessionInputs: Map + messageOutputs: Map } diff --git a/tests/handlers/spans.test.ts b/tests/handlers/spans.test.ts index 15429dd..cf0aa1a 100644 --- a/tests/handlers/spans.test.ts +++ b/tests/handlers/spans.test.ts @@ -1,5 +1,20 @@ import { describe, test, expect } from "bun:test" import { SpanStatusCode } from "@opentelemetry/api" +import { + AGENT_NAME, + LLM_MODEL_NAME, + LLM_PROVIDER, + LLM_SYSTEM, + LLM_TOKEN_COUNT_COMPLETION, + LLM_TOKEN_COUNT_COMPLETION_DETAILS_REASONING, + LLM_TOKEN_COUNT_PROMPT, + LLM_TOKEN_COUNT_PROMPT_DETAILS_CACHE_READ, + LLM_TOKEN_COUNT_PROMPT_DETAILS_CACHE_WRITE, + OpenInferenceSpanKind, + SemanticConventions, + SESSION_ID, + TOOL_NAME, +} from "@arizeai/openinference-semantic-conventions" import type { Span } from "@opentelemetry/api" import { handleSessionCreated, handleSessionIdle, handleSessionError } from "../../src/handlers/session.ts" import { handleMessageUpdated, handleMessagePartUpdated, startMessageSpan } from "../../src/handlers/message.ts" @@ -12,6 +27,8 @@ import type { EventMessagePartUpdated, } from "@opencode-ai/sdk" +const OPENINFERENCE_SPAN_KIND = SemanticConventions.OPENINFERENCE_SPAN_KIND + function makeSessionCreated(sessionID: string, createdAt = 1000, parentID?: string): EventSessionCreated { return { type: "session.created", @@ -92,6 +109,14 @@ describe("session spans", () => { const { ctx, tracer } = makeCtx() handleSessionCreated(makeSessionCreated("ses_1"), ctx) expect(tracer.spans[0]!.attributes["session.id"]).toBe("ses_1") + expect(tracer.spans[0]!.attributes[SESSION_ID]).toBe("ses_1") + }) + + test("session span is tagged as an OpenInference agent span", () => { + const { ctx, tracer } = makeCtx() + handleSessionCreated(makeSessionCreated("ses_1"), ctx) + expect(tracer.spans[0]!.attributes[OPENINFERENCE_SPAN_KIND]).toBe(OpenInferenceSpanKind.AGENT) + expect(tracer.spans[0]!.attributes[AGENT_NAME]).toBe("unknown") }) test("session span carries is_subagent=false for root session", () => { @@ -188,6 +213,8 @@ describe("tool spans", () => { const { ctx, tracer } = makeCtx() handleMessagePartUpdated(makeToolPartUpdated("running", { tool: "read_file" }), ctx) expect(tracer.spans[0]!.attributes["tool.name"]).toBe("read_file") + expect(tracer.spans[0]!.attributes[TOOL_NAME]).toBe("read_file") + expect(tracer.spans[0]!.attributes[OPENINFERENCE_SPAN_KIND]).toBe(OpenInferenceSpanKind.TOOL) }) test("ends tool span with OK status on completion", () => { @@ -260,19 +287,21 @@ describe("tool spans", () => { }) describe("message (LLM) spans", () => { - test("startMessageSpan creates a gen_ai.chat span", () => { + test("startMessageSpan creates an llm span", () => { const { ctx, tracer } = makeCtx() startMessageSpan("ses_1", "msg_1", "claude-3-5-sonnet", "anthropic", 1000, ctx) expect(tracer.spans).toHaveLength(1) - expect(tracer.spans[0]!.name).toBe("gen_ai.chat") + expect(tracer.spans[0]!.name).toBe("opencode.llm") expect(ctx.messageSpans.has("ses_1:msg_1")).toBe(true) }) - test("startMessageSpan sets gen_ai.* attributes", () => { + test("startMessageSpan sets OpenInference LLM attributes", () => { const { ctx, tracer } = makeCtx() startMessageSpan("ses_1", "msg_1", "gpt-4o", "openai", 1000, ctx) - expect(tracer.spans[0]!.attributes["gen_ai.system"]).toBe("openai") - expect(tracer.spans[0]!.attributes["gen_ai.request.model"]).toBe("gpt-4o") + expect(tracer.spans[0]!.attributes[OPENINFERENCE_SPAN_KIND]).toBe(OpenInferenceSpanKind.LLM) + expect(tracer.spans[0]!.attributes[LLM_SYSTEM]).toBe("openai") + expect(tracer.spans[0]!.attributes[LLM_PROVIDER]).toBe("openai") + expect(tracer.spans[0]!.attributes[LLM_MODEL_NAME]).toBe("gpt-4o") }) test("startMessageSpan is a no-op when span already exists for sessionID:messageID", () => { @@ -307,7 +336,7 @@ describe("message (LLM) spans", () => { expect(tracer.spans[0]!.status.message).toBe("RateLimitError") }) - test("handleMessageUpdated sets gen_ai.usage token attributes on span", () => { + test("handleMessageUpdated sets OpenInference token attributes on span", () => { const { ctx, tracer } = makeCtx() startMessageSpan("ses_1", "msg_1", "claude-3-5-sonnet", "anthropic", 1000, ctx) handleMessageUpdated( @@ -318,11 +347,11 @@ describe("message (LLM) spans", () => { ctx, ) const span = tracer.spans[0]! - expect(span.attributes["gen_ai.usage.input_tokens"]).toBe(200) - expect(span.attributes["gen_ai.usage.output_tokens"]).toBe(80) - expect(span.attributes["gen_ai.usage.reasoning_tokens"]).toBe(10) - expect(span.attributes["gen_ai.usage.cache_read_tokens"]).toBe(30) - expect(span.attributes["gen_ai.usage.cache_creation_tokens"]).toBe(5) + expect(span.attributes[LLM_TOKEN_COUNT_PROMPT]).toBe(200) + expect(span.attributes[LLM_TOKEN_COUNT_COMPLETION]).toBe(80) + expect(span.attributes[LLM_TOKEN_COUNT_COMPLETION_DETAILS_REASONING]).toBe(10) + expect(span.attributes[LLM_TOKEN_COUNT_PROMPT_DETAILS_CACHE_READ]).toBe(30) + expect(span.attributes[LLM_TOKEN_COUNT_PROMPT_DETAILS_CACHE_WRITE]).toBe(5) }) test("handleMessageUpdated no-ops span handling when no span exists for messageID", () => { @@ -339,7 +368,7 @@ describe("message (LLM) spans", () => { handleSessionCreated(makeSessionCreated("ses_1"), ctx) startMessageSpan("ses_1", "msg_1", "claude", "anthropic", 1000, ctx) expect(tracer.spans).toHaveLength(2) - expect(tracer.spans[1]!.name).toBe("gen_ai.chat") + expect(tracer.spans[1]!.name).toBe("opencode.llm") expect(tracer.spans[1]!.parentSpan).toBe(tracer.spans[0]) }) }) @@ -384,7 +413,7 @@ describe("orphaned span cleanup", () => { startMessageSpan("ses_1", "msg_orphan", "claude", "anthropic", 1000, ctx) handleSessionIdle(makeSessionIdle("ses_1"), ctx) expect(ctx.messageSpans.has("ses_1:msg_orphan")).toBe(false) - const msgSpan = tracer.spans.find(s => s.name === "gen_ai.chat")! + const msgSpan = tracer.spans.find(s => s.name === "opencode.llm")! expect(msgSpan.ended).toBe(true) expect(msgSpan.status.code).toBe(SpanStatusCode.ERROR) }) @@ -395,7 +424,7 @@ describe("orphaned span cleanup", () => { startMessageSpan("ses_1", "msg_orphan", "claude", "anthropic", 1000, ctx) handleSessionError(makeSessionError("ses_1"), ctx) expect(ctx.messageSpans.has("ses_1:msg_orphan")).toBe(false) - const msgSpan = tracer.spans.find(s => s.name === "gen_ai.chat")! + const msgSpan = tracer.spans.find(s => s.name === "opencode.llm")! expect(msgSpan.ended).toBe(true) expect(msgSpan.status.code).toBe(SpanStatusCode.ERROR) }) @@ -438,7 +467,7 @@ describe("OPENCODE_DISABLE_TRACES=session", () => { handleSessionCreated(makeSessionCreated("ses_1"), ctx) startMessageSpan("ses_1", "msg_1", "claude", "anthropic", 1000, ctx) expect(tracer.spans).toHaveLength(1) - expect(tracer.spans[0]!.name).toBe("gen_ai.chat") + expect(tracer.spans[0]!.name).toBe("opencode.llm") }) }) diff --git a/tests/helpers.ts b/tests/helpers.ts index cc7ddbb..6170963 100644 --- a/tests/helpers.ts +++ b/tests/helpers.ts @@ -187,6 +187,8 @@ export function makeCtx(projectID = "proj_test", disabledMetrics: string[] = [], tracePrefix: "opencode.", sessionSpans: new Map(), messageSpans: new Map(), + sessionInputs: new Map(), + messageOutputs: new Map(), } return {