@inference/tracing 0.0.20 → 0.0.21

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1,5 +1,6 @@
1
1
  import { SpanKind, type Span, type Tracer } from "@opentelemetry/api";
2
2
  import { type SpanKindValue } from "./semconv.ts";
3
+ import { type TokenUsage } from "./span-helpers.ts";
3
4
  /**
4
5
  * Options accepted by {@link agentSpan}. The minimum useful call is
5
6
  * `agentSpan(tracer, { agentId: "support-agent" }, fn)`.
@@ -56,10 +57,70 @@ export interface AgentSpanOptions {
56
57
  */
57
58
  sessionId?: string;
58
59
  }
60
+ /**
61
+ * Options accepted by {@link manualSpan}.
62
+ *
63
+ * `manualSpan` is the general-purpose helper for framework gaps: custom
64
+ * routers, provider failover attempts, tool executors, evaluators,
65
+ * postprocessors, or high-level agent loops. {@link agentSpan} is the
66
+ * AGENT-flavored wrapper around it.
67
+ */
68
+ export interface ManualSpanOptions {
69
+ /** Operation span name — appears as the row label in the trace viewer. */
70
+ spanName: string;
71
+ /**
72
+ * OpenInference span kind. Defaults to `CHAIN` — pass `TOOL`, `RETRIEVER`,
73
+ * `AGENT`, etc. when authoring the equivalent shape by hand.
74
+ */
75
+ spanKind?: SpanKindValue;
76
+ /** OTel-level span kind. Defaults to `INTERNAL`. */
77
+ otelKind?: SpanKind;
78
+ /**
79
+ * Provider identifier — e.g. `"openai"`, `"anthropic"`. Becomes
80
+ * `gen_ai.system` when set.
81
+ */
82
+ system?: string;
83
+ /** Stable agent identifier for AGENT-flavored manual spans. */
84
+ agentId?: string;
85
+ /** Optional display label. */
86
+ agentName?: string;
87
+ /** Optional role within a multi-agent workflow. */
88
+ agentRole?: string;
89
+ /**
90
+ * Stable id for one conversation. Becomes this span's `session.id`
91
+ * so downstream viewers can group manual spans by conversation.
92
+ */
93
+ sessionId?: string;
94
+ /** Convenience: set `input.value` from the callback. Strings pass through; objects are JSON-stringified. */
95
+ input?: unknown;
96
+ /** Convenience: set `output.value` before the callback runs (rare — usually set inside the callback). */
97
+ output?: unknown;
98
+ /** Convenience: set `llm.model_name`. */
99
+ model?: string;
100
+ /**
101
+ * Provider-shaped usage object. Accepted shapes match
102
+ * {@link normalizeUsage} (OpenAI Chat/Responses and Anthropic).
103
+ */
104
+ usage?: unknown;
105
+ /** Convenience: set `tool.name` on a TOOL-kind manual span. */
106
+ toolName?: string;
107
+ /** Convenience: set `tool_call.id` on a TOOL-kind manual span. */
108
+ toolCallId?: string;
109
+ /**
110
+ * Custom attributes applied with OTel-safe normalization — primitives and
111
+ * primitive arrays pass through; objects and mixed arrays are
112
+ * JSON-stringified.
113
+ */
114
+ attributes?: Record<string, unknown>;
115
+ /** Free-form metadata bag — JSON-stringified onto the `metadata` attribute. */
116
+ metadata?: Record<string, unknown>;
117
+ /** Free-form string tags — set on the `tags` attribute as a string array. */
118
+ tags?: readonly string[];
119
+ }
59
120
  /**
60
121
  * The handle passed into the callback function. Provides type-safe
61
- * helpers for the OI attribute set the agent span should carry, plus
62
- * a `raw` escape hatch for callers that need the underlying OTel
122
+ * helpers for the OI attribute set the agent/manual span should carry,
123
+ * plus a `raw` escape hatch for callers that need the underlying OTel
63
124
  * `Span` (e.g. to add custom attributes outside the OI vocabulary).
64
125
  */
65
126
  export interface AgentSpanHandle {
@@ -67,18 +128,18 @@ export interface AgentSpanHandle {
67
128
  * Record the agent's input. Strings are stored as-is on
68
129
  * `input.value`; non-strings are JSON-stringified.
69
130
  */
70
- setInput(value: string | object): void;
131
+ setInput(value: unknown): void;
71
132
  /**
72
133
  * Record the agent's final output. Same string-vs-JSON rule as
73
134
  * {@link AgentSpanHandle.setInput}.
74
135
  */
75
- setOutput(value: string | object): void;
136
+ setOutput(value: unknown): void;
76
137
  /**
77
- * Record token usage on the agent span. Any of `prompt`,
78
- * `completion`, `total` may be omitted; only the provided fields
79
- * are written. Use this when you have aggregated counts across
80
- * multiple LLM calls inside the agent loop — per-call counts are
81
- * captured separately by the LLM-level instrumentation.
138
+ * Record token usage on the span. Any of `prompt`, `completion`,
139
+ * `total` may be omitted; only the provided fields are written. Use this
140
+ * when you have aggregated counts across multiple LLM calls inside the
141
+ * agent loop — per-call counts are captured separately by the LLM-level
142
+ * instrumentation.
82
143
  */
83
144
  recordTokens(tokens: {
84
145
  prompt?: number;
@@ -86,12 +147,88 @@ export interface AgentSpanHandle {
86
147
  total?: number;
87
148
  }): void;
88
149
  /**
89
- * Record the model name the agent used. Becomes `llm.model_name`.
150
+ * Record token usage from a provider-shaped usage object (OpenAI Chat /
151
+ * Responses, or Anthropic). Returns the normalized {@link TokenUsage}.
152
+ */
153
+ recordUsage(usage: unknown): TokenUsage;
154
+ /**
155
+ * Record the model name. Becomes `llm.model_name`.
90
156
  */
91
157
  setModel(model: string): void;
158
+ /**
159
+ * Record tool identity on a TOOL-kind span.
160
+ */
161
+ setTool(options: {
162
+ name: string;
163
+ callId?: string;
164
+ }): void;
165
+ /**
166
+ * Set one span attribute with OTel-safe value normalization. Mixed
167
+ * arrays and plain objects are JSON-stringified; primitives and
168
+ * primitive arrays pass through.
169
+ */
170
+ setAttribute(key: string, value: unknown): void;
171
+ /**
172
+ * Set many span attributes with the same normalization as
173
+ * {@link AgentSpanHandle.setAttribute}.
174
+ */
175
+ setAttributes(attributes: Record<string, unknown>): void;
92
176
  /** Escape hatch — the underlying OTel `Span`. */
93
177
  raw: Span;
94
178
  }
179
+ /**
180
+ * Wrap custom work in an OpenInference-shaped manual span.
181
+ *
182
+ * Use this for framework gaps: custom routers, provider failover attempts,
183
+ * tool executors, evaluators, postprocessors, or high-level agent loops.
184
+ * Sets the OpenInference span kind, normalizes input/output values, accepts
185
+ * provider-shaped usage payloads, and keeps the body inside the active
186
+ * OTel context so SDK auto-instrumentation nests under it.
187
+ *
188
+ * Status semantics:
189
+ * - On callback success: span ends with `OK`.
190
+ * - On thrown error: the error is recorded as a span event, the
191
+ * span ends with `ERROR`, and the original exception re-throws.
192
+ *
193
+ * @example A custom TOOL span around a function the SDK can't see
194
+ * ```ts
195
+ * await manualSpan(
196
+ * tracing.tracer,
197
+ * {
198
+ * spanName: "submit_question",
199
+ * spanKind: SpanKindValues.TOOL,
200
+ * toolName: "submit_question",
201
+ * toolCallId: callId,
202
+ * input: { question },
203
+ * },
204
+ * async (span) => {
205
+ * const result = await submitQuestion(question);
206
+ * span.setOutput(result);
207
+ * },
208
+ * );
209
+ * ```
210
+ *
211
+ * @example A CHAIN step that aggregates usage across several LLM calls
212
+ * ```ts
213
+ * await manualSpan(
214
+ * tracing.tracer,
215
+ * {
216
+ * spanName: "question_generation/bloom",
217
+ * spanKind: SpanKindValues.CHAIN,
218
+ * system: "fireworks",
219
+ * model: "accounts/fireworks/models/gpt-oss-120b",
220
+ * usage: aggregatedUsage,
221
+ * metadata: { deckId: "deck-123" },
222
+ * tags: ["question-gen-agent", "template:bloom"],
223
+ * },
224
+ * async (span) => {
225
+ * const questions = await generateQuestions();
226
+ * span.setOutput({ questionCount: questions.length });
227
+ * },
228
+ * );
229
+ * ```
230
+ */
231
+ export declare function manualSpan<T>(tracer: Tracer, options: ManualSpanOptions, fn: (span: AgentSpanHandle) => Promise<T> | T): Promise<T>;
95
232
  /**
96
233
  * Wrap a chunk of agent work in an OpenInference-shaped AGENT span.
97
234
  *
@@ -1,6 +1,123 @@
1
1
  import { context, SpanKind, SpanStatusCode, trace, } from "@opentelemetry/api";
2
2
  import { Attr, SpanKindValues } from "./semconv.js";
3
- import { contextWithAgentIdentity } from "./active-agent-context.js";
3
+ import { contextWithAgentIdentity, getActiveAgentIdentity, } from "./active-agent-context.js";
4
+ import { recordSpanUsage, setSpanAttribute, setSpanAttributes, stringifyValue, } from "./span-helpers.js";
5
+ /**
6
+ * Wrap custom work in an OpenInference-shaped manual span.
7
+ *
8
+ * Use this for framework gaps: custom routers, provider failover attempts,
9
+ * tool executors, evaluators, postprocessors, or high-level agent loops.
10
+ * Sets the OpenInference span kind, normalizes input/output values, accepts
11
+ * provider-shaped usage payloads, and keeps the body inside the active
12
+ * OTel context so SDK auto-instrumentation nests under it.
13
+ *
14
+ * Status semantics:
15
+ * - On callback success: span ends with `OK`.
16
+ * - On thrown error: the error is recorded as a span event, the
17
+ * span ends with `ERROR`, and the original exception re-throws.
18
+ *
19
+ * @example A custom TOOL span around a function the SDK can't see
20
+ * ```ts
21
+ * await manualSpan(
22
+ * tracing.tracer,
23
+ * {
24
+ * spanName: "submit_question",
25
+ * spanKind: SpanKindValues.TOOL,
26
+ * toolName: "submit_question",
27
+ * toolCallId: callId,
28
+ * input: { question },
29
+ * },
30
+ * async (span) => {
31
+ * const result = await submitQuestion(question);
32
+ * span.setOutput(result);
33
+ * },
34
+ * );
35
+ * ```
36
+ *
37
+ * @example A CHAIN step that aggregates usage across several LLM calls
38
+ * ```ts
39
+ * await manualSpan(
40
+ * tracing.tracer,
41
+ * {
42
+ * spanName: "question_generation/bloom",
43
+ * spanKind: SpanKindValues.CHAIN,
44
+ * system: "fireworks",
45
+ * model: "accounts/fireworks/models/gpt-oss-120b",
46
+ * usage: aggregatedUsage,
47
+ * metadata: { deckId: "deck-123" },
48
+ * tags: ["question-gen-agent", "template:bloom"],
49
+ * },
50
+ * async (span) => {
51
+ * const questions = await generateQuestions();
52
+ * span.setOutput({ questionCount: questions.length });
53
+ * },
54
+ * );
55
+ * ```
56
+ */
57
+ export async function manualSpan(tracer, options, fn) {
58
+ const spanKind = options.spanKind ?? SpanKindValues.CHAIN;
59
+ const otelKind = options.otelKind ?? SpanKind.INTERNAL;
60
+ const spanName = options.spanName;
61
+ const activeIdentity = getActiveAgentIdentity();
62
+ const agentId = options.agentId ?? activeIdentity?.id;
63
+ const agentName = options.agentName ?? activeIdentity?.name;
64
+ const agentRole = options.agentRole ?? activeIdentity?.role;
65
+ const startAttributes = {
66
+ [Attr.SPAN_KIND]: spanKind,
67
+ };
68
+ if (agentId != null)
69
+ startAttributes[Attr.AGENT_ID] = agentId;
70
+ if (agentName != null)
71
+ startAttributes[Attr.AGENT_NAME] = agentName;
72
+ if (agentRole != null)
73
+ startAttributes[Attr.AGENT_ROLE] = agentRole;
74
+ if (options.system != null)
75
+ startAttributes[Attr.SYSTEM] = options.system;
76
+ if (options.toolName != null)
77
+ startAttributes[Attr.TOOL_NAME] = options.toolName;
78
+ if (options.toolCallId != null)
79
+ startAttributes[Attr.TOOL_CALL_ID] = options.toolCallId;
80
+ const sessionId = options.sessionId?.trim();
81
+ if (sessionId)
82
+ startAttributes[Attr.SESSION_ID] = sessionId;
83
+ if (options.attributes)
84
+ Object.assign(startAttributes, options.attributes);
85
+ if (options.metadata && Object.keys(options.metadata).length > 0) {
86
+ startAttributes.metadata = options.metadata;
87
+ }
88
+ if (options.tags && options.tags.length > 0) {
89
+ startAttributes.tags = Array.from(options.tags);
90
+ }
91
+ const span = tracer.startSpan(spanName, { kind: otelKind });
92
+ setSpanAttributes(span, startAttributes);
93
+ const handle = makeHandle(span);
94
+ if (options.input !== undefined)
95
+ handle.setInput(options.input);
96
+ if (options.output !== undefined)
97
+ handle.setOutput(options.output);
98
+ if (options.model != null)
99
+ handle.setModel(options.model);
100
+ if (options.usage !== undefined)
101
+ handle.recordUsage(options.usage);
102
+ const ctx = contextWithAgentIdentity(trace.setSpan(context.active(), span), {
103
+ id: agentId,
104
+ name: agentName,
105
+ role: agentRole,
106
+ });
107
+ try {
108
+ const result = await context.with(ctx, () => fn(handle));
109
+ span.setStatus({ code: SpanStatusCode.OK });
110
+ return result;
111
+ }
112
+ catch (err) {
113
+ span.recordException(err);
114
+ span.setStatus({ code: SpanStatusCode.ERROR, message: String(err) });
115
+ throw err;
116
+ }
117
+ finally {
118
+ span.end();
119
+ }
120
+ }
4
121
  /**
5
122
  * Wrap a chunk of agent work in an OpenInference-shaped AGENT span.
6
123
  *
@@ -61,35 +178,31 @@ import { contextWithAgentIdentity } from "./active-agent-context.js";
61
178
  * ```
62
179
  */
63
180
  export async function agentSpan(tracer, options, fn) {
64
- const spanKind = options.spanKind ?? SpanKindValues.AGENT;
65
- const otelKind = options.otelKind ?? SpanKind.INTERNAL;
66
181
  const agentId = options.agentId ?? options.id;
67
182
  const agentName = options.agentName ?? options.name;
68
183
  const spanName = resolveAgentSpanName(options, agentId, agentName);
69
- const attributes = {
70
- [Attr.SPAN_KIND]: spanKind,
71
- };
72
- if (agentId != null)
73
- attributes[Attr.AGENT_ID] = agentId;
74
- if (agentName != null)
75
- attributes[Attr.AGENT_NAME] = agentName;
76
- if (options.role != null)
77
- attributes[Attr.AGENT_ROLE] = options.role;
78
- if (options.system != null)
79
- attributes[Attr.SYSTEM] = options.system;
80
- const sessionId = options.sessionId?.trim();
81
- if (sessionId)
82
- attributes[Attr.SESSION_ID] = sessionId;
83
- const span = tracer.startSpan(spanName, {
84
- kind: otelKind,
85
- attributes,
86
- });
87
- const handle = {
184
+ return manualSpan(tracer, {
185
+ spanName,
186
+ spanKind: options.spanKind ?? SpanKindValues.AGENT,
187
+ otelKind: options.otelKind ?? SpanKind.INTERNAL,
188
+ system: options.system,
189
+ agentId,
190
+ agentName,
191
+ agentRole: options.role,
192
+ sessionId: options.sessionId,
193
+ }, fn);
194
+ }
195
+ function makeHandle(span) {
196
+ return {
88
197
  setInput(value) {
89
- span.setAttribute(Attr.INPUT_VALUE, typeof value === "string" ? value : JSON.stringify(value));
198
+ if (value === undefined)
199
+ return;
200
+ span.setAttribute(Attr.INPUT_VALUE, stringifyValue(value));
90
201
  },
91
202
  setOutput(value) {
92
- span.setAttribute(Attr.OUTPUT_VALUE, typeof value === "string" ? value : JSON.stringify(value));
203
+ if (value === undefined)
204
+ return;
205
+ span.setAttribute(Attr.OUTPUT_VALUE, stringifyValue(value));
93
206
  },
94
207
  recordTokens({ prompt, completion, total }) {
95
208
  if (prompt != null)
@@ -99,34 +212,25 @@ export async function agentSpan(tracer, options, fn) {
99
212
  if (total != null)
100
213
  span.setAttribute(Attr.TOKEN_COUNT_TOTAL, total);
101
214
  },
215
+ recordUsage(usage) {
216
+ return recordSpanUsage(span, usage);
217
+ },
102
218
  setModel(model) {
103
219
  span.setAttribute(Attr.MODEL_NAME, model);
104
220
  },
221
+ setTool({ name, callId }) {
222
+ span.setAttribute(Attr.TOOL_NAME, name);
223
+ if (callId != null)
224
+ span.setAttribute(Attr.TOOL_CALL_ID, callId);
225
+ },
226
+ setAttribute(key, value) {
227
+ setSpanAttribute(span, key, value);
228
+ },
229
+ setAttributes(attributes) {
230
+ setSpanAttributes(span, attributes);
231
+ },
105
232
  raw: span,
106
233
  };
107
- // Run the callback inside the span's active context so any child
108
- // spans created during it (LLM calls, tool calls, custom spans)
109
- // auto-parent to the AGENT span. Without this, downstream
110
- // instrumentation creates orphan spans that don't visually nest in
111
- // the trace tree.
112
- const ctx = contextWithAgentIdentity(trace.setSpan(context.active(), span), {
113
- id: agentId,
114
- name: agentName,
115
- role: options.role,
116
- });
117
- try {
118
- const result = await context.with(ctx, () => fn(handle));
119
- span.setStatus({ code: SpanStatusCode.OK });
120
- return result;
121
- }
122
- catch (err) {
123
- span.recordException(err);
124
- span.setStatus({ code: SpanStatusCode.ERROR, message: String(err) });
125
- throw err;
126
- }
127
- finally {
128
- span.end();
129
- }
130
234
  }
131
235
  function resolveAgentSpanName(options, agentId, agentName) {
132
236
  const explicitName = normalize(options.spanName);
package/dist/index.d.ts CHANGED
@@ -29,7 +29,8 @@
29
29
  * - **First-party OpenInference instrumentation** for openai,
30
30
  * anthropic, langchain, langgraph, langsmith, openai-agents, claude-agent-sdk,
31
31
  * cursor-sdk, ai-sdk, livekit-agents, and pi-ai.
32
- * - **Helpers**: {@link agentSpan} for manual AGENT spans;
32
+ * - **Helpers**: {@link manualSpan} for custom CHAIN/TOOL/RETRIEVER spans,
33
+ * {@link agentSpan} for manual AGENT spans;
33
34
  * {@link wrapClaudeAgentSdkQuery} for the one SDK whose ESM
34
35
  * namespace can't be patched in place.
35
36
  *
@@ -45,8 +46,10 @@
45
46
  */
46
47
  export { setup } from "./setup.ts";
47
48
  export type { CatalystTracing, InstrumentModules, SetupOptions, } from "./setup.ts";
48
- export { agentSpan } from "./agent-span.ts";
49
- export type { AgentSpanHandle, AgentSpanOptions } from "./agent-span.ts";
49
+ export { agentSpan, manualSpan } from "./agent-span.ts";
50
+ export type { AgentSpanHandle, AgentSpanOptions, ManualSpanOptions, } from "./agent-span.ts";
51
+ export { normalizeUsage, recordSpanUsage, setSpanAttribute, setSpanAttributes, stringifyValue, } from "./span-helpers.ts";
52
+ export type { TokenUsage } from "./span-helpers.ts";
50
53
  export { agentIdentityAttributes, applyActiveAgentIdentity, contextWithAgentIdentity, getActiveAgentIdentity, } from "./active-agent-context.ts";
51
54
  export type { AgentIdentity } from "./active-agent-context.ts";
52
55
  export { wrapClaudeAgentSdkQuery } from "./instrumentation/claude-agent-sdk.ts";
package/dist/index.js CHANGED
@@ -29,7 +29,8 @@
29
29
  * - **First-party OpenInference instrumentation** for openai,
30
30
  * anthropic, langchain, langgraph, langsmith, openai-agents, claude-agent-sdk,
31
31
  * cursor-sdk, ai-sdk, livekit-agents, and pi-ai.
32
- * - **Helpers**: {@link agentSpan} for manual AGENT spans;
32
+ * - **Helpers**: {@link manualSpan} for custom CHAIN/TOOL/RETRIEVER spans,
33
+ * {@link agentSpan} for manual AGENT spans;
33
34
  * {@link wrapClaudeAgentSdkQuery} for the one SDK whose ESM
34
35
  * namespace can't be patched in place.
35
36
  *
@@ -44,7 +45,8 @@
44
45
  * @packageDocumentation
45
46
  */
46
47
  export { setup } from "./setup.js";
47
- export { agentSpan } from "./agent-span.js";
48
+ export { agentSpan, manualSpan } from "./agent-span.js";
49
+ export { normalizeUsage, recordSpanUsage, setSpanAttribute, setSpanAttributes, stringifyValue, } from "./span-helpers.js";
48
50
  export { agentIdentityAttributes, applyActiveAgentIdentity, contextWithAgentIdentity, getActiveAgentIdentity, } from "./active-agent-context.js";
49
51
  export { wrapClaudeAgentSdkQuery } from "./instrumentation/claude-agent-sdk.js";
50
52
  export { instrumentElevenLabs } from "./entrypoints/elevenlabs.js";
@@ -0,0 +1,66 @@
1
+ /**
2
+ * Public helpers for authoring rich manual spans.
3
+ *
4
+ * The SDK's auto-instrumentations normalize JSON-ish values, usage objects,
5
+ * and OTel-safe attributes internally. These helpers expose the same
6
+ * ergonomics to manual span authors so applications do not have to copy
7
+ * small coercion utilities into every integration.
8
+ */
9
+ import type { Span } from "@opentelemetry/api";
10
+ /**
11
+ * Normalized token usage extracted from provider-shaped usage payloads.
12
+ *
13
+ * Fields are optional; only the ones present in the source payload are set.
14
+ */
15
+ export interface TokenUsage {
16
+ prompt?: number;
17
+ completion?: number;
18
+ total?: number;
19
+ promptCacheWrite?: number;
20
+ promptCacheRead?: number;
21
+ completionReasoning?: number;
22
+ }
23
+ /**
24
+ * Return the string representation used for OpenInference IO attributes.
25
+ *
26
+ * Strings pass through; `undefined` and `null` both serialize to `"null"`
27
+ * (matching Python's `json.dumps(None)` behavior so equivalent payloads
28
+ * produce equivalent attributes across SDKs). Everything else is
29
+ * `JSON.stringify`ed. If stringification throws (e.g. circular reference),
30
+ * falls back to `String()`.
31
+ */
32
+ export declare function stringifyValue(value: unknown): string;
33
+ /**
34
+ * Set one OTel attribute, JSON-encoding unsupported structured values.
35
+ *
36
+ * `null`/`undefined` are dropped. Primitives and primitive arrays are set as-is;
37
+ * mixed arrays and plain objects are JSON-stringified.
38
+ */
39
+ export declare function setSpanAttribute(span: Span, key: string, value: unknown): void;
40
+ /**
41
+ * Set several OTel attributes with the same normalization as one value.
42
+ *
43
+ * `null`/`undefined` values are skipped.
44
+ */
45
+ export declare function setSpanAttributes(span: Span, attributes: Record<string, unknown>): void;
46
+ /**
47
+ * Record token attributes from OpenAI- and Anthropic-shaped usage objects.
48
+ *
49
+ * Supported input keys include OpenAI Chat Completions/Responses style
50
+ * `prompt_tokens` / `completion_tokens` / `total_tokens`, newer
51
+ * `input_tokens` / `output_tokens` spellings, OpenAI detail objects such as
52
+ * `prompt_tokens_details.cached_tokens` and
53
+ * `completion_tokens_details.reasoning_tokens`, and Anthropic cache accounting
54
+ * keys such as `cache_creation_input_tokens` and `cache_read_input_tokens`.
55
+ *
56
+ * Returns the normalized {@link TokenUsage} so callers can read the numbers
57
+ * back without re-parsing.
58
+ */
59
+ export declare function recordSpanUsage(span: Span, usage: unknown): TokenUsage;
60
+ /**
61
+ * Normalize common provider usage payloads without touching a span.
62
+ *
63
+ * Same key support as {@link recordSpanUsage}. Returns an empty
64
+ * {@link TokenUsage} for non-object input.
65
+ */
66
+ export declare function normalizeUsage(usage: unknown): TokenUsage;
@@ -0,0 +1,191 @@
1
+ import { Attr } from "./semconv.js";
2
+ /**
3
+ * Return the string representation used for OpenInference IO attributes.
4
+ *
5
+ * Strings pass through; `undefined` and `null` both serialize to `"null"`
6
+ * (matching Python's `json.dumps(None)` behavior so equivalent payloads
7
+ * produce equivalent attributes across SDKs). Everything else is
8
+ * `JSON.stringify`ed. If stringification throws (e.g. circular reference),
9
+ * falls back to `String()`.
10
+ */
11
+ export function stringifyValue(value) {
12
+ if (typeof value === "string")
13
+ return value;
14
+ if (value === undefined)
15
+ return "null";
16
+ try {
17
+ return JSON.stringify(value) ?? "null";
18
+ }
19
+ catch {
20
+ return String(value);
21
+ }
22
+ }
23
+ /**
24
+ * Set one OTel attribute, JSON-encoding unsupported structured values.
25
+ *
26
+ * `null`/`undefined` are dropped. Primitives and primitive arrays are set as-is;
27
+ * mixed arrays and plain objects are JSON-stringified.
28
+ */
29
+ export function setSpanAttribute(span, key, value) {
30
+ const coerced = coerceAttributeValue(value);
31
+ if (coerced !== undefined) {
32
+ span.setAttribute(key, coerced);
33
+ }
34
+ }
35
+ /**
36
+ * Set several OTel attributes with the same normalization as one value.
37
+ *
38
+ * `null`/`undefined` values are skipped.
39
+ */
40
+ export function setSpanAttributes(span, attributes) {
41
+ for (const [key, value] of Object.entries(attributes)) {
42
+ if (value != null) {
43
+ setSpanAttribute(span, key, value);
44
+ }
45
+ }
46
+ }
47
+ /**
48
+ * Record token attributes from OpenAI- and Anthropic-shaped usage objects.
49
+ *
50
+ * Supported input keys include OpenAI Chat Completions/Responses style
51
+ * `prompt_tokens` / `completion_tokens` / `total_tokens`, newer
52
+ * `input_tokens` / `output_tokens` spellings, OpenAI detail objects such as
53
+ * `prompt_tokens_details.cached_tokens` and
54
+ * `completion_tokens_details.reasoning_tokens`, and Anthropic cache accounting
55
+ * keys such as `cache_creation_input_tokens` and `cache_read_input_tokens`.
56
+ *
57
+ * Returns the normalized {@link TokenUsage} so callers can read the numbers
58
+ * back without re-parsing.
59
+ */
60
+ export function recordSpanUsage(span, usage) {
61
+ const normalized = normalizeUsage(usage);
62
+ if (normalized.prompt !== undefined) {
63
+ span.setAttribute(Attr.TOKEN_COUNT_PROMPT, normalized.prompt);
64
+ }
65
+ if (normalized.completion !== undefined) {
66
+ span.setAttribute(Attr.TOKEN_COUNT_COMPLETION, normalized.completion);
67
+ }
68
+ if (normalized.total !== undefined) {
69
+ span.setAttribute(Attr.TOKEN_COUNT_TOTAL, normalized.total);
70
+ }
71
+ if (normalized.promptCacheWrite !== undefined) {
72
+ span.setAttribute(Attr.TOKEN_COUNT_PROMPT_CACHE_WRITE, normalized.promptCacheWrite);
73
+ }
74
+ if (normalized.promptCacheRead !== undefined) {
75
+ span.setAttribute(Attr.TOKEN_COUNT_PROMPT_CACHE_READ, normalized.promptCacheRead);
76
+ }
77
+ if (normalized.completionReasoning !== undefined) {
78
+ span.setAttribute(Attr.TOKEN_COUNT_COMPLETION_REASONING, normalized.completionReasoning);
79
+ }
80
+ return normalized;
81
+ }
82
+ /**
83
+ * Normalize common provider usage payloads without touching a span.
84
+ *
85
+ * Same key support as {@link recordSpanUsage}. Returns an empty
86
+ * {@link TokenUsage} for non-object input.
87
+ */
88
+ export function normalizeUsage(usage) {
89
+ const source = asRecord(usage);
90
+ if (source == null)
91
+ return {};
92
+ let prompt = firstInt(source, "prompt_tokens", "input_tokens");
93
+ const completion = firstInt(source, "completion_tokens", "output_tokens");
94
+ let total = firstInt(source, "total_tokens");
95
+ const promptDetails = firstRecord(source, "prompt_tokens_details", "input_tokens_details") ?? {};
96
+ const completionDetails = firstRecord(source, "completion_tokens_details", "output_tokens_details") ?? {};
97
+ const promptCacheWrite = firstInt(source, "cache_creation_input_tokens");
98
+ const promptCacheRead = firstInt(source, "cache_read_input_tokens") ??
99
+ firstInt(promptDetails, "cached_tokens");
100
+ const completionReasoning = firstInt(completionDetails, "reasoning_tokens");
101
+ if (hasAnthropicCacheUsage(source) && prompt !== undefined) {
102
+ prompt = prompt + (promptCacheWrite ?? 0) + (promptCacheRead ?? 0);
103
+ }
104
+ if (total === undefined && prompt !== undefined && completion !== undefined) {
105
+ total = prompt + completion;
106
+ }
107
+ const out = {};
108
+ if (prompt !== undefined)
109
+ out.prompt = prompt;
110
+ if (completion !== undefined)
111
+ out.completion = completion;
112
+ if (total !== undefined)
113
+ out.total = total;
114
+ if (promptCacheWrite !== undefined)
115
+ out.promptCacheWrite = promptCacheWrite;
116
+ if (promptCacheRead !== undefined)
117
+ out.promptCacheRead = promptCacheRead;
118
+ if (completionReasoning !== undefined)
119
+ out.completionReasoning = completionReasoning;
120
+ return out;
121
+ }
122
+ function coerceAttributeValue(value) {
123
+ if (value == null)
124
+ return undefined;
125
+ if (typeof value === "string" || typeof value === "boolean")
126
+ return value;
127
+ if (typeof value === "number")
128
+ return Number.isFinite(value) ? value : stringifyValue(value);
129
+ if (Array.isArray(value)) {
130
+ if (value.length === 0)
131
+ return [];
132
+ if (value.every((item) => typeof item === "string"))
133
+ return value;
134
+ if (value.every((item) => typeof item === "boolean"))
135
+ return value;
136
+ if (value.every((item) => typeof item === "number" && Number.isFinite(item))) {
137
+ return value;
138
+ }
139
+ return stringifyValue(value);
140
+ }
141
+ if (typeof value === "object")
142
+ return stringifyValue(value);
143
+ return stringifyValue(value);
144
+ }
145
+ function asRecord(value) {
146
+ if (value == null || typeof value !== "object" || Array.isArray(value))
147
+ return null;
148
+ return value;
149
+ }
150
+ function firstRecord(source, ...keys) {
151
+ for (const key of keys) {
152
+ const candidate = asRecord(source[key]);
153
+ if (candidate != null)
154
+ return candidate;
155
+ }
156
+ return undefined;
157
+ }
158
+ function firstInt(source, ...keys) {
159
+ if (source == null)
160
+ return undefined;
161
+ for (const key of keys) {
162
+ const coerced = coerceInt(source[key]);
163
+ if (coerced !== undefined)
164
+ return coerced;
165
+ }
166
+ return undefined;
167
+ }
168
+ function coerceInt(value) {
169
+ if (value == null || typeof value === "boolean")
170
+ return undefined;
171
+ if (typeof value === "number") {
172
+ if (!Number.isFinite(value))
173
+ return undefined;
174
+ if (Number.isInteger(value))
175
+ return value;
176
+ return undefined;
177
+ }
178
+ if (typeof value === "string") {
179
+ const trimmed = value.trim();
180
+ if (trimmed === "")
181
+ return undefined;
182
+ if (!/^-?\d+$/.test(trimmed))
183
+ return undefined;
184
+ const parsed = Number.parseInt(trimmed, 10);
185
+ return Number.isFinite(parsed) ? parsed : undefined;
186
+ }
187
+ return undefined;
188
+ }
189
+ function hasAnthropicCacheUsage(source) {
190
+ return (source.cache_creation_input_tokens != null || source.cache_read_input_tokens != null);
191
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@inference/tracing",
3
- "version": "0.0.20",
3
+ "version": "0.0.21",
4
4
  "type": "module",
5
5
  "description": "First-party OpenInference-shaped tracing for TypeScript LLM and agent applications on Catalyst by Inference.net.",
6
6
  "homepage": "https://inference.net",