@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.
- package/dist/agent-span.d.ts +147 -10
- package/dist/agent-span.js +151 -47
- package/dist/index.d.ts +6 -3
- package/dist/index.js +4 -2
- package/dist/span-helpers.d.ts +66 -0
- package/dist/span-helpers.js +191 -0
- package/package.json +1 -1
package/dist/agent-span.d.ts
CHANGED
|
@@ -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,
|
|
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:
|
|
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:
|
|
136
|
+
setOutput(value: unknown): void;
|
|
76
137
|
/**
|
|
77
|
-
* Record token usage on the
|
|
78
|
-
* `
|
|
79
|
-
*
|
|
80
|
-
*
|
|
81
|
-
*
|
|
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
|
|
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
|
*
|
package/dist/agent-span.js
CHANGED
|
@@ -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
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
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
|
-
|
|
198
|
+
if (value === undefined)
|
|
199
|
+
return;
|
|
200
|
+
span.setAttribute(Attr.INPUT_VALUE, stringifyValue(value));
|
|
90
201
|
},
|
|
91
202
|
setOutput(value) {
|
|
92
|
-
|
|
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
|
|
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
|
|
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.
|
|
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",
|