@apo-ai/sdk 0.16.1 → 0.17.0
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-task/integrations/ai-sdk.d.ts +1 -1
- package/dist/agent-task/integrations/anthropic.d.ts +1 -1
- package/dist/agent-task/integrations/anthropic.js +2 -2
- package/dist/agent-task/integrations/openai.d.ts +1 -1
- package/dist/agent-task/integrations/openai.js +2 -2
- package/dist/agent-task/public.d.ts +367 -13
- package/dist/agent-task/public.js +832 -59
- package/dist/agent-task/public.js.map +1 -1
- package/dist/{chunk-4BQSCD6T.js → chunk-6U6V7VKZ.js} +4 -3
- package/dist/{chunk-4BQSCD6T.js.map → chunk-6U6V7VKZ.js.map} +1 -1
- package/dist/{chunk-YWUTUQPJ.js → chunk-CSOY6OSP.js} +63 -13
- package/dist/chunk-CSOY6OSP.js.map +1 -0
- package/dist/{chunk-JN7RHBTS.js → chunk-KWIYQSWK.js} +5 -1
- package/dist/chunk-KWIYQSWK.js.map +1 -0
- package/dist/{chunk-NSMKSPBU.js → chunk-Z7DVZ34X.js} +4 -3
- package/dist/{chunk-NSMKSPBU.js.map → chunk-Z7DVZ34X.js.map} +1 -1
- package/dist/otel/index.js +1 -1
- package/dist/{tracing-CEucJqQI.d.ts → tracing-CDeEfMis.d.ts} +24 -1
- package/package.json +1 -1
- package/dist/chunk-JN7RHBTS.js.map +0 -1
- package/dist/chunk-YWUTUQPJ.js.map +0 -1
|
@@ -4,7 +4,7 @@ import {
|
|
|
4
4
|
emitGenerationAndTools,
|
|
5
5
|
servingHostFromBaseURL,
|
|
6
6
|
startGeneration
|
|
7
|
-
} from "./chunk-
|
|
7
|
+
} from "./chunk-KWIYQSWK.js";
|
|
8
8
|
|
|
9
9
|
// src/agent-task/integrations/anthropic.ts
|
|
10
10
|
function createApoAnthropic(client, options) {
|
|
@@ -27,7 +27,8 @@ function createApoAnthropic(client, options) {
|
|
|
27
27
|
parentSpanId,
|
|
28
28
|
taskId,
|
|
29
29
|
turnNumber,
|
|
30
|
-
...serving
|
|
30
|
+
...serving,
|
|
31
|
+
apiProvider: serving.provider
|
|
31
32
|
});
|
|
32
33
|
try {
|
|
33
34
|
const response = await realCreate(params);
|
|
@@ -78,4 +79,4 @@ function createApoAnthropic(client, options) {
|
|
|
78
79
|
export {
|
|
79
80
|
createApoAnthropic
|
|
80
81
|
};
|
|
81
|
-
//# sourceMappingURL=chunk-
|
|
82
|
+
//# sourceMappingURL=chunk-6U6V7VKZ.js.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../src/agent-task/integrations/anthropic.ts"],"sourcesContent":["/**\n * Anthropic JS SDK tracing integration for apo.\n *\n * Wraps an `Anthropic` client so `messages.create()` calls are\n * automatically traced. Tool calls and generations are captured without\n * any manual span code in the adapter.\n *\n * @example\n * ```ts\n * import Anthropic from \"@anthropic-ai/sdk\";\n * import { createApoAnthropic } from \"@apo-ai/sdk/agent-task\";\n *\n * async sendUserTurn(turn, { trace, parentSpanId }) {\n * const client = createApoAnthropic(\n * new Anthropic({ apiKey }),\n * { trace, parentSpanId, taskId: ctx.task.id, turnNumber }\n * );\n * const response = await client.messages.create({\n * model: \"claude-sonnet-4-20250514\",\n * max_tokens: 1024,\n * messages: [{ role: \"user\", content: String(turn) }],\n * tools: [...],\n * });\n * const text = response.content.filter(b => b.type === \"text\").map(b => b.text).join(\"\");\n * return { response: text };\n * }\n * ```\n *\n * @module\n */\n\nimport type { AgentTaskTraceContext } from \"../tracing.ts\";\nimport {\n startGeneration,\n emitGenerationAndTools,\n servingHostFromBaseURL,\n} from \"./span-helpers.ts\";\n\nexport interface CreateApoAnthropicOptions {\n /** The tee'd trace context from `sendUserTurn`'s second argument. */\n trace: AgentTaskTraceContext;\n /** The parent span id for this turn (from `sendUserTurn`'s context). */\n parentSpanId?: string;\n /** Optional task id, surfaced in span metadata. */\n taskId?: string;\n /** Optional turn number, surfaced in span metadata. */\n turnNumber?: number;\n}\n\n/**\n * Minimal structural shape of the Anthropic client we intercept.\n */\ninterface AnthropicClientLike {\n messages: {\n // eslint-disable-next-line @typescript-eslint/no-explicit-any\n create: (...args: any[]) => Promise<any>;\n };\n // eslint-disable-next-line @typescript-eslint/no-explicit-any\n [key: string]: any;\n}\n\n/**\n * The response shape we read from `messages.create()`.\n */\ninterface AnthropicResponseLike {\n model?: string;\n content?: Array<\n | { type: \"text\"; text: string }\n | { type: \"tool_use\"; name: string; input?: unknown }\n | { type: string }\n >;\n usage?: {\n input_tokens?: number;\n output_tokens?: number;\n };\n}\n\n/**\n * Wrap an Anthropic client so `messages.create()` is traced.\n * Returns a Proxy-wrapped client — call it exactly as you would the\n * original. Non-streaming only; streaming responses pass through\n * untraced.\n *\n * For SDKs that emit OpenTelemetry natively (Claude Agent SDK, Vercel AI\n * SDK), prefer `configureApoTelemetry()` instead — no wrapper needed.\n * This manual wrapper is for the raw `@anthropic-ai/sdk` package or\n * custom clients that don't emit OTel on their own.\n */\nexport function createApoAnthropic<T extends AnthropicClientLike>(\n client: T,\n options: CreateApoAnthropicOptions,\n): T {\n const { trace, parentSpanId, taskId, turnNumber } = options;\n const realCreate = client.messages.create.bind(client.messages);\n\n // eslint-disable-next-line @typescript-eslint/no-explicit-any\n const tracedCreate = async (...args: any[]): Promise<any> => {\n const params = (args[0] ?? {}) as Record<string, unknown>;\n const model = String(params.model ?? \"unknown\");\n\n // Non-streaming: trace fully. Streaming (stream: true): pass through.\n if (params.stream === true) {\n return realCreate(params);\n }\n\n // The Anthropic SDK can also be pointed at a gateway via baseURL —\n // resolve the host it actually calls (issue #307).\n const serving = servingHostFromBaseURL(\n (client as { baseURL?: unknown }).baseURL,\n \"anthropic\",\n );\n const { spanId, startedAt } = startGeneration(trace, {\n model,\n system: typeof params.system === \"string\" ? params.system : undefined,\n messages: params.messages,\n parentSpanId,\n taskId,\n turnNumber,\n ...serving,\n });\n\n try {\n const response = (await realCreate(params)) as AnthropicResponseLike;\n const blocks = response.content ?? [];\n const text = blocks\n .filter((b): b is { type: \"text\"; text: string } => b.type === \"text\")\n .map((b) => b.text)\n .join(\"\\n\");\n const toolCalls = blocks\n .filter(\n (b): b is { type: \"tool_use\"; name: string; input?: unknown } =>\n b.type === \"tool_use\",\n )\n .map((b) => ({\n name: b.name,\n input: b.input,\n }));\n\n emitGenerationAndTools(trace, spanId, startedAt, {\n text: text || undefined,\n servedModel: response.model,\n promptTokens: response.usage?.input_tokens,\n completionTokens: response.usage?.output_tokens,\n toolCalls,\n taskId,\n turnNumber,\n });\n\n return response;\n } catch (error) {\n emitGenerationAndTools(trace, spanId, startedAt, {\n taskId,\n turnNumber,\n error: { message: error instanceof Error ? error.message : String(error) },\n });\n throw error;\n }\n };\n\n // Proxy that intercepts client.messages.create but passes\n // everything else through unchanged.\n return new Proxy(client, {\n get(target, prop, receiver) {\n if (prop === \"messages\") {\n return new Proxy(target.messages, {\n get(messagesTarget, messagesProp) {\n if (messagesProp === \"create\") {\n return tracedCreate;\n }\n return Reflect.get(messagesTarget, messagesProp);\n },\n });\n }\n return Reflect.get(target, prop, receiver);\n },\n }) as T;\n}\n"],"mappings":";;;;;;;;;AAwFO,SAAS,mBACd,QACA,SACG;AACH,QAAM,EAAE,OAAO,cAAc,QAAQ,WAAW,IAAI;AACpD,QAAM,aAAa,OAAO,SAAS,OAAO,KAAK,OAAO,QAAQ;AAG9D,QAAM,eAAe,UAAU,SAA8B;AAC3D,UAAM,SAAU,KAAK,CAAC,KAAK,CAAC;AAC5B,UAAM,QAAQ,OAAO,OAAO,SAAS,SAAS;AAG9C,QAAI,OAAO,WAAW,MAAM;AAC1B,aAAO,WAAW,MAAM;AAAA,IAC1B;AAIA,UAAM,UAAU;AAAA,MACb,OAAiC;AAAA,MAClC;AAAA,IACF;AACA,UAAM,EAAE,QAAQ,UAAU,IAAI,gBAAgB,OAAO;AAAA,MACnD;AAAA,MACA,QAAQ,OAAO,OAAO,WAAW,WAAW,OAAO,SAAS;AAAA,MAC5D,UAAU,OAAO;AAAA,MACjB;AAAA,MACA;AAAA,MACA;AAAA,MACA,GAAG;AAAA,
|
|
1
|
+
{"version":3,"sources":["../src/agent-task/integrations/anthropic.ts"],"sourcesContent":["/**\n * Anthropic JS SDK tracing integration for apo.\n *\n * Wraps an `Anthropic` client so `messages.create()` calls are\n * automatically traced. Tool calls and generations are captured without\n * any manual span code in the adapter.\n *\n * @example\n * ```ts\n * import Anthropic from \"@anthropic-ai/sdk\";\n * import { createApoAnthropic } from \"@apo-ai/sdk/agent-task\";\n *\n * async sendUserTurn(turn, { trace, parentSpanId }) {\n * const client = createApoAnthropic(\n * new Anthropic({ apiKey }),\n * { trace, parentSpanId, taskId: ctx.task.id, turnNumber }\n * );\n * const response = await client.messages.create({\n * model: \"claude-sonnet-4-20250514\",\n * max_tokens: 1024,\n * messages: [{ role: \"user\", content: String(turn) }],\n * tools: [...],\n * });\n * const text = response.content.filter(b => b.type === \"text\").map(b => b.text).join(\"\");\n * return { response: text };\n * }\n * ```\n *\n * @module\n */\n\nimport type { AgentTaskTraceContext } from \"../tracing.ts\";\nimport {\n startGeneration,\n emitGenerationAndTools,\n servingHostFromBaseURL,\n} from \"./span-helpers.ts\";\n\nexport interface CreateApoAnthropicOptions {\n /** The tee'd trace context from `sendUserTurn`'s second argument. */\n trace: AgentTaskTraceContext;\n /** The parent span id for this turn (from `sendUserTurn`'s context). */\n parentSpanId?: string;\n /** Optional task id, surfaced in span metadata. */\n taskId?: string;\n /** Optional turn number, surfaced in span metadata. */\n turnNumber?: number;\n}\n\n/**\n * Minimal structural shape of the Anthropic client we intercept.\n */\ninterface AnthropicClientLike {\n messages: {\n // eslint-disable-next-line @typescript-eslint/no-explicit-any\n create: (...args: any[]) => Promise<any>;\n };\n // eslint-disable-next-line @typescript-eslint/no-explicit-any\n [key: string]: any;\n}\n\n/**\n * The response shape we read from `messages.create()`.\n */\ninterface AnthropicResponseLike {\n model?: string;\n content?: Array<\n | { type: \"text\"; text: string }\n | { type: \"tool_use\"; name: string; input?: unknown }\n | { type: string }\n >;\n usage?: {\n input_tokens?: number;\n output_tokens?: number;\n };\n}\n\n/**\n * Wrap an Anthropic client so `messages.create()` is traced.\n * Returns a Proxy-wrapped client — call it exactly as you would the\n * original. Non-streaming only; streaming responses pass through\n * untraced.\n *\n * For SDKs that emit OpenTelemetry natively (Claude Agent SDK, Vercel AI\n * SDK), prefer `configureApoTelemetry()` instead — no wrapper needed.\n * This manual wrapper is for the raw `@anthropic-ai/sdk` package or\n * custom clients that don't emit OTel on their own.\n */\nexport function createApoAnthropic<T extends AnthropicClientLike>(\n client: T,\n options: CreateApoAnthropicOptions,\n): T {\n const { trace, parentSpanId, taskId, turnNumber } = options;\n const realCreate = client.messages.create.bind(client.messages);\n\n // eslint-disable-next-line @typescript-eslint/no-explicit-any\n const tracedCreate = async (...args: any[]): Promise<any> => {\n const params = (args[0] ?? {}) as Record<string, unknown>;\n const model = String(params.model ?? \"unknown\");\n\n // Non-streaming: trace fully. Streaming (stream: true): pass through.\n if (params.stream === true) {\n return realCreate(params);\n }\n\n // The Anthropic SDK can also be pointed at a gateway via baseURL —\n // resolve the host it actually calls (issue #307).\n const serving = servingHostFromBaseURL(\n (client as { baseURL?: unknown }).baseURL,\n \"anthropic\",\n );\n const { spanId, startedAt } = startGeneration(trace, {\n model,\n system: typeof params.system === \"string\" ? params.system : undefined,\n messages: params.messages,\n parentSpanId,\n taskId,\n turnNumber,\n ...serving,\n apiProvider: serving.provider,\n });\n\n try {\n const response = (await realCreate(params)) as AnthropicResponseLike;\n const blocks = response.content ?? [];\n const text = blocks\n .filter((b): b is { type: \"text\"; text: string } => b.type === \"text\")\n .map((b) => b.text)\n .join(\"\\n\");\n const toolCalls = blocks\n .filter(\n (b): b is { type: \"tool_use\"; name: string; input?: unknown } =>\n b.type === \"tool_use\",\n )\n .map((b) => ({\n name: b.name,\n input: b.input,\n }));\n\n emitGenerationAndTools(trace, spanId, startedAt, {\n text: text || undefined,\n servedModel: response.model,\n promptTokens: response.usage?.input_tokens,\n completionTokens: response.usage?.output_tokens,\n toolCalls,\n taskId,\n turnNumber,\n });\n\n return response;\n } catch (error) {\n emitGenerationAndTools(trace, spanId, startedAt, {\n taskId,\n turnNumber,\n error: { message: error instanceof Error ? error.message : String(error) },\n });\n throw error;\n }\n };\n\n // Proxy that intercepts client.messages.create but passes\n // everything else through unchanged.\n return new Proxy(client, {\n get(target, prop, receiver) {\n if (prop === \"messages\") {\n return new Proxy(target.messages, {\n get(messagesTarget, messagesProp) {\n if (messagesProp === \"create\") {\n return tracedCreate;\n }\n return Reflect.get(messagesTarget, messagesProp);\n },\n });\n }\n return Reflect.get(target, prop, receiver);\n },\n }) as T;\n}\n"],"mappings":";;;;;;;;;AAwFO,SAAS,mBACd,QACA,SACG;AACH,QAAM,EAAE,OAAO,cAAc,QAAQ,WAAW,IAAI;AACpD,QAAM,aAAa,OAAO,SAAS,OAAO,KAAK,OAAO,QAAQ;AAG9D,QAAM,eAAe,UAAU,SAA8B;AAC3D,UAAM,SAAU,KAAK,CAAC,KAAK,CAAC;AAC5B,UAAM,QAAQ,OAAO,OAAO,SAAS,SAAS;AAG9C,QAAI,OAAO,WAAW,MAAM;AAC1B,aAAO,WAAW,MAAM;AAAA,IAC1B;AAIA,UAAM,UAAU;AAAA,MACb,OAAiC;AAAA,MAClC;AAAA,IACF;AACA,UAAM,EAAE,QAAQ,UAAU,IAAI,gBAAgB,OAAO;AAAA,MACnD;AAAA,MACA,QAAQ,OAAO,OAAO,WAAW,WAAW,OAAO,SAAS;AAAA,MAC5D,UAAU,OAAO;AAAA,MACjB;AAAA,MACA;AAAA,MACA;AAAA,MACA,GAAG;AAAA,MACH,aAAa,QAAQ;AAAA,IACvB,CAAC;AAED,QAAI;AACF,YAAM,WAAY,MAAM,WAAW,MAAM;AACzC,YAAM,SAAS,SAAS,WAAW,CAAC;AACpC,YAAM,OAAO,OACV,OAAO,CAAC,MAA2C,EAAE,SAAS,MAAM,EACpE,IAAI,CAAC,MAAM,EAAE,IAAI,EACjB,KAAK,IAAI;AACZ,YAAM,YAAY,OACf;AAAA,QACC,CAAC,MACC,EAAE,SAAS;AAAA,MACf,EACC,IAAI,CAAC,OAAO;AAAA,QACX,MAAM,EAAE;AAAA,QACR,OAAO,EAAE;AAAA,MACX,EAAE;AAEJ,6BAAuB,OAAO,QAAQ,WAAW;AAAA,QAC/C,MAAM,QAAQ;AAAA,QACd,aAAa,SAAS;AAAA,QACtB,cAAc,SAAS,OAAO;AAAA,QAC9B,kBAAkB,SAAS,OAAO;AAAA,QAClC;AAAA,QACA;AAAA,QACA;AAAA,MACF,CAAC;AAED,aAAO;AAAA,IACT,SAAS,OAAO;AACd,6BAAuB,OAAO,QAAQ,WAAW;AAAA,QAC/C;AAAA,QACA;AAAA,QACA,OAAO,EAAE,SAAS,iBAAiB,QAAQ,MAAM,UAAU,OAAO,KAAK,EAAE;AAAA,MAC3E,CAAC;AACD,YAAM;AAAA,IACR;AAAA,EACF;AAIA,SAAO,IAAI,MAAM,QAAQ;AAAA,IACvB,IAAI,QAAQ,MAAM,UAAU;AAC1B,UAAI,SAAS,YAAY;AACvB,eAAO,IAAI,MAAM,OAAO,UAAU;AAAA,UAChC,IAAI,gBAAgB,cAAc;AAChC,gBAAI,iBAAiB,UAAU;AAC7B,qBAAO;AAAA,YACT;AACA,mBAAO,QAAQ,IAAI,gBAAgB,YAAY;AAAA,UACjD;AAAA,QACF,CAAC;AAAA,MACH;AACA,aAAO,QAAQ,IAAI,QAAQ,MAAM,QAAQ;AAAA,IAC3C;AAAA,EACF,CAAC;AACH;","names":[]}
|
|
@@ -61,6 +61,22 @@ function monotonicNowMs() {
|
|
|
61
61
|
function round3(n) {
|
|
62
62
|
return Math.round(n * 1e3) / 1e3;
|
|
63
63
|
}
|
|
64
|
+
function servingFromTelemetryMetadata(attributes) {
|
|
65
|
+
const prefix = "ai.telemetry.metadata.apo.llm.";
|
|
66
|
+
const keys = ["api_provider", "serving_host", "route", "service_tier"];
|
|
67
|
+
let serving;
|
|
68
|
+
for (const key of keys) {
|
|
69
|
+
const value = attributes[prefix + key];
|
|
70
|
+
if (typeof value === "string" && value.trim() !== "") {
|
|
71
|
+
serving ??= {};
|
|
72
|
+
if (key === "api_provider") serving.apiProvider = value;
|
|
73
|
+
else if (key === "serving_host") serving.servingHost = value;
|
|
74
|
+
else if (key === "route") serving.route = value;
|
|
75
|
+
else serving.serviceTier = value;
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
return serving;
|
|
79
|
+
}
|
|
64
80
|
function translateOtelSpan(spanName, data) {
|
|
65
81
|
const { attributes, status } = data;
|
|
66
82
|
const isError = status.code === 2;
|
|
@@ -83,6 +99,7 @@ function translateOtelSpan(spanName, data) {
|
|
|
83
99
|
model: String(
|
|
84
100
|
attributes["gen_ai.request.model"] ?? attributes["ai.model.id"] ?? attributes["ai.response.model"] ?? "unknown"
|
|
85
101
|
),
|
|
102
|
+
serving: servingFromTelemetryMetadata(attributes),
|
|
86
103
|
...extractTokenCounts(attributes),
|
|
87
104
|
error: isError,
|
|
88
105
|
errorMessage
|
|
@@ -150,6 +167,7 @@ var ApoSpanProcessor = class {
|
|
|
150
167
|
constructor() {
|
|
151
168
|
this.spanMap = /* @__PURE__ */ new Map();
|
|
152
169
|
this.servedModelByParent = /* @__PURE__ */ new Map();
|
|
170
|
+
this.servingByParent = /* @__PURE__ */ new Map();
|
|
153
171
|
}
|
|
154
172
|
onStart(span, _parentContext) {
|
|
155
173
|
const run = getActiveApoRun();
|
|
@@ -169,6 +187,9 @@ var ApoSpanProcessor = class {
|
|
|
169
187
|
step_name: translated.stepName,
|
|
170
188
|
model: translated.model ?? translated.stepName,
|
|
171
189
|
observation_type: translated.observationType,
|
|
190
|
+
...translated.serving?.apiProvider ? { api_provider: translated.serving.apiProvider } : {},
|
|
191
|
+
...translated.serving?.servingHost ? { serving_host: translated.serving.servingHost } : {},
|
|
192
|
+
...translated.serving?.route ? { route: translated.serving.route } : {},
|
|
172
193
|
...translated.input ? { input: translated.input } : {},
|
|
173
194
|
metadata: {
|
|
174
195
|
...run.taskId ? { taskId: run.taskId } : {},
|
|
@@ -189,6 +210,14 @@ var ApoSpanProcessor = class {
|
|
|
189
210
|
if (parentOtelId && served) {
|
|
190
211
|
this.servedModelByParent.set(parentOtelId, served);
|
|
191
212
|
}
|
|
213
|
+
const declaredServing = servingFromTelemetryMetadata(span.attributes ?? {});
|
|
214
|
+
if (parentOtelId && declaredServing) {
|
|
215
|
+
this.servingByParent.set(parentOtelId, declaredServing);
|
|
216
|
+
}
|
|
217
|
+
const servedForThis = served ?? this.servedModelByParent.get(otelId);
|
|
218
|
+
this.servedModelByParent.delete(otelId);
|
|
219
|
+
const servingForThis = declaredServing ?? this.servingByParent.get(otelId);
|
|
220
|
+
this.servingByParent.delete(otelId);
|
|
192
221
|
const tracked = this.spanMap.get(otelId);
|
|
193
222
|
if (!tracked) return;
|
|
194
223
|
this.spanMap.delete(otelId);
|
|
@@ -199,12 +228,30 @@ var ApoSpanProcessor = class {
|
|
|
199
228
|
status: span.status ?? { code: 0 }
|
|
200
229
|
});
|
|
201
230
|
const latency = round3(monotonicNowMs() - tracked.startedAt);
|
|
202
|
-
|
|
203
|
-
|
|
231
|
+
if (servingForThis) {
|
|
232
|
+
const raw = span;
|
|
233
|
+
if (servingForThis.apiProvider) {
|
|
234
|
+
raw.setAttribute?.("apo.llm.api_provider", servingForThis.apiProvider);
|
|
235
|
+
}
|
|
236
|
+
if (servingForThis.servingHost) {
|
|
237
|
+
raw.setAttribute?.("apo.llm.serving_host", servingForThis.servingHost);
|
|
238
|
+
}
|
|
239
|
+
if (servingForThis.route) {
|
|
240
|
+
raw.setAttribute?.("apo.llm.route", servingForThis.route);
|
|
241
|
+
}
|
|
242
|
+
if (servingForThis.serviceTier) {
|
|
243
|
+
raw.setAttribute?.("apo.llm.service_tier", servingForThis.serviceTier);
|
|
244
|
+
}
|
|
245
|
+
if (tracked.otelSpanName === "ai.generateText" || tracked.otelSpanName === "ai.streamText") {
|
|
246
|
+
raw.setAttribute?.("apo.llm.request_kind", "aggregate");
|
|
247
|
+
}
|
|
248
|
+
}
|
|
204
249
|
run.trace.endSpan(tracked.apoSpanId, {
|
|
205
250
|
latency_ms: latency,
|
|
206
251
|
...buildEndParams(translated),
|
|
207
|
-
...servedForThis ? { served_model: servedForThis } : {}
|
|
252
|
+
...servedForThis ? { served_model: servedForThis } : {},
|
|
253
|
+
...servingForThis?.apiProvider ? { api_provider: servingForThis.apiProvider } : {},
|
|
254
|
+
...servingForThis?.servingHost ? { serving_host: servingForThis.servingHost } : {}
|
|
208
255
|
});
|
|
209
256
|
}
|
|
210
257
|
forceFlush() {
|
|
@@ -212,12 +259,15 @@ var ApoSpanProcessor = class {
|
|
|
212
259
|
}
|
|
213
260
|
shutdown() {
|
|
214
261
|
this.spanMap.clear();
|
|
262
|
+
this.servedModelByParent.clear();
|
|
263
|
+
this.servingByParent.clear();
|
|
215
264
|
return Promise.resolve();
|
|
216
265
|
}
|
|
217
266
|
/** Clear all state — for testing. */
|
|
218
267
|
reset() {
|
|
219
268
|
this.spanMap.clear();
|
|
220
269
|
this.servedModelByParent.clear();
|
|
270
|
+
this.servingByParent.clear();
|
|
221
271
|
}
|
|
222
272
|
};
|
|
223
273
|
function buildEndParams(t) {
|
|
@@ -329,16 +379,16 @@ async function configureApoTelemetry(options) {
|
|
|
329
379
|
if (options.serviceVersion) resourceAttrs[SEMRESATTRS_SERVICE_VERSION] = options.serviceVersion;
|
|
330
380
|
if (options.environment) resourceAttrs[SEMRESATTRS_DEPLOYMENT_ENVIRONMENT] = options.environment;
|
|
331
381
|
const resource = resourceFromAttributes(resourceAttrs);
|
|
332
|
-
const spanProcessors = [createApoSpanProcessor({
|
|
333
|
-
endpoint,
|
|
334
|
-
headers,
|
|
335
|
-
processor: options.processor,
|
|
336
|
-
scheduledDelayMillis: options.scheduledDelayMillis
|
|
337
|
-
})];
|
|
338
382
|
const apoProcessor = getRegisteredApoProcessor();
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
383
|
+
const spanProcessors = [
|
|
384
|
+
...apoProcessor ? [apoProcessor] : [],
|
|
385
|
+
createApoSpanProcessor({
|
|
386
|
+
endpoint,
|
|
387
|
+
headers,
|
|
388
|
+
processor: options.processor,
|
|
389
|
+
scheduledDelayMillis: options.scheduledDelayMillis
|
|
390
|
+
})
|
|
391
|
+
];
|
|
342
392
|
const provider = new BasicTracerProvider({
|
|
343
393
|
resource,
|
|
344
394
|
spanProcessors
|
|
@@ -528,4 +578,4 @@ export {
|
|
|
528
578
|
injectTraceparent,
|
|
529
579
|
extractTraceparent
|
|
530
580
|
};
|
|
531
|
-
//# sourceMappingURL=chunk-
|
|
581
|
+
//# sourceMappingURL=chunk-CSOY6OSP.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../src/otel/index.ts","../src/agent-task/integrations/run-context.ts","../src/agent-task/integrations/otel-translate.ts","../src/agent-task/integrations/otel-processor.ts","../src/agent-task/integrations/register.ts","../src/apo-auth.ts"],"sourcesContent":["/**\n * OTel-native tracing setup for the apo SDK.\n *\n * This module provides the standard OpenTelemetry setup path for apo:\n * - ``configureApoTelemetry`` creates a tracer provider with an OTLP exporter\n * - ``withApoTrace`` creates root/child spans using standard OTel context\n * - Helper functions (``traceTool``, ``traceAgent``, etc.) emit normal OTel\n * spans with GenAI or apo vendor attributes\n *\n * This replaced the removed ``TraceTracker`` custom event protocol — apo\n * tracing is OTel-only now.\n *\n * @module @apo-ai/sdk/otel\n */\n\nimport {\n trace,\n context,\n defaultTextMapGetter,\n defaultTextMapSetter,\n diag,\n DiagConsoleLogger,\n DiagLogLevel,\n type Span,\n type Tracer,\n type SpanOptions,\n type TracerProvider,\n} from \"@opentelemetry/api\";\nimport { W3CTraceContextPropagator } from \"@opentelemetry/core\";\n\n/**\n * Shared W3C Trace Context propagator. OTel's official implementation handles\n * `traceparent`/`tracestate`, edge cases (invalid traceId/spanId, versioning),\n * and stays correct as the spec evolves — so we delegate to it instead of\n * hand-rolling `00-<trace>-<span>-<flags>` parsing.\n */\nconst w3cPropagator = new W3CTraceContextPropagator();\nimport {\n BasicTracerProvider,\n BatchSpanProcessor,\n SimpleSpanProcessor,\n type SpanExporter,\n type SpanProcessor,\n} from \"@opentelemetry/sdk-trace-base\";\nimport { AsyncHooksContextManager } from \"@opentelemetry/context-async-hooks\";\nimport { resourceFromAttributes } from \"@opentelemetry/resources\";\nimport {\n SEMRESATTRS_SERVICE_NAME,\n SEMRESATTRS_SERVICE_VERSION,\n SEMRESATTRS_SERVICE_NAMESPACE,\n SEMRESATTRS_DEPLOYMENT_ENVIRONMENT,\n} from \"@opentelemetry/semantic-conventions\";\nimport { OTLPTraceExporter } from \"@opentelemetry/exporter-trace-otlp-http\";\nimport { getRegisteredApoProcessor } from \"../agent-task/integrations/register.ts\";\nimport { resolveApoAuthHeaders } from \"../apo-auth.ts\";\n\n// ── public types ───────────────────────────────────────────\n\nexport interface ConfigureApoTelemetryOptions {\n /** Explicit permission for this standalone helper to own OTel lifecycle. */\n takeOwnership: true;\n /**\n * Full OTLP traces endpoint URL. Falls back to the `APO_OTLP_ENDPOINT` env\n * var when omitted, then to `http://localhost:8000/api/public/otel/v1/traces`.\n */\n endpoint?: string;\n /**\n * Diagnostic resource attribute only. Auth owns tenancy — the project is\n * determined by the API key / service token, never by this field.\n * Falls back to the `APO_PROJECT` env var when omitted.\n */\n project?: string;\n /**\n * Service name for the OTel resource (required). Defaults to `\"apo-agent\"`.\n */\n serviceName?: string;\n /** Service version for the OTel resource. */\n serviceVersion?: string;\n /** Deployment environment (production, staging, etc.). */\n environment?: string;\n /**\n * Auth headers (Authorization: Basic/Bearer). Falls back to headers derived\n * from `APO_PUBLIC_KEY`+`APO_SECRET_KEY` (Basic) or `APO_AUTH_TOKEN` (Bearer)\n * env vars when omitted. Use {@link buildApoAuthHeaders} to build them\n * explicitly.\n */\n headers?: Record<string, string>;\n /**\n * Public key for Basic auth. Falls back to `APO_PUBLIC_KEY` env var.\n * Used with `secretKey` to build `Authorization: Basic base64(pk:sk)`.\n */\n publicKey?: string;\n /**\n * Secret key for Basic auth. Falls back to `APO_SECRET_KEY` env var.\n */\n secretKey?: string;\n /**\n * Bearer auth token. Falls back to `APO_AUTH_TOKEN` env var. Used only when\n * `publicKey`/`secretKey` are absent.\n */\n authToken?: string;\n /**\n * @deprecated OTel JS 2.x providers cannot accept processors after\n * construction. Use `createApoSpanProcessor()` in the host provider's\n * `spanProcessors` array instead.\n */\n provider?: TracerProvider;\n /** Batch for services; simple is useful for short-lived task subprocesses. */\n processor?: \"batch\" | \"simple\";\n /**\n * Override the batch processor's flush interval (ms). Leave undefined to\n * honor `OTEL_BSP_SCHEDULE_DELAY` (or OTel's 5s default).\n */\n scheduledDelayMillis?: number;\n /**\n * Whether to register apo's provider as the global tracer provider. Defaults\n * to false. When true, registration only happens if no global provider is\n * registered yet — apo never silently replaces an existing global provider.\n */\n registerGlobal?: boolean;\n}\n\nexport interface ApoTelemetryHandle {\n /** The OTel tracer for creating spans. */\n tracer: Tracer;\n /** The provider, for advanced use. */\n provider: TracerProvider;\n /** Export all ended spans without relinquishing provider ownership. */\n forceFlush(): Promise<void>;\n /** Gracefully shut down the provider and flush pending spans. */\n shutdown(): Promise<void>;\n}\n\nexport interface ApoTraceExporterOptions {\n /** Full standard OTLP/HTTP traces endpoint. */\n endpoint: string;\n /** Authentication headers sent by the official OTLP exporter. */\n headers: Record<string, string>;\n}\n\nexport interface ApoSpanProcessorOptions extends ApoTraceExporterOptions {\n /** Batch for long-lived services (default), simple for short-lived jobs. */\n processor?: \"batch\" | \"simple\";\n /**\n * Override the batch processor's flush interval (ms). Leave undefined to\n * honor `OTEL_BSP_SCHEDULE_DELAY` (or OTel's 5s default).\n */\n scheduledDelayMillis?: number;\n}\n\nexport interface ApoTraceOptions {\n /** Span name. */\n name: string;\n /** apo.observation.type override. */\n observationType?: string;\n /** Additional attributes. */\n attributes?: Record<string, unknown>;\n}\n\ninterface OwnedContextState {\n manager: AsyncHooksContextManager;\n references: number;\n}\n\nlet ownedContext: OwnedContextState | null = null;\n\n/** Create the official OTLP/HTTP exporter for a host-owned OTel provider. */\nexport function createApoTraceExporter(\n options: ApoTraceExporterOptions,\n): SpanExporter {\n return new OTLPTraceExporter({\n url: options.endpoint,\n headers: options.headers,\n });\n}\n\n/**\n * Create an apo OTLP processor for inclusion when constructing a host provider.\n * This factory has no global side effects and transfers lifecycle ownership to\n * the host provider.\n */\nexport function createApoSpanProcessor(\n options: ApoSpanProcessorOptions,\n): SpanProcessor {\n const exporter = createApoTraceExporter(options);\n if (options.processor === \"simple\") {\n return new SimpleSpanProcessor(exporter);\n }\n return new BatchSpanProcessor(exporter, {\n ...(options.scheduledDelayMillis !== undefined && {\n scheduledDelayMillis: options.scheduledDelayMillis,\n }),\n });\n}\n\n// ── auth + env-var helpers (mirror apo-otel-python) ─────────────────────\n\nconst DEFAULT_APO_OTLP_ENDPOINT = \"http://localhost:8000/api/public/otel/v1/traces\";\n\n/**\n * Build the `Authorization` header for apo's OTLP endpoint.\n *\n * - If `publicKey` + `secretKey` are given (or read from `APO_PUBLIC_KEY` /\n * `APO_SECRET_KEY`), returns HTTP Basic: `Basic base64(pk:sk)`. * both halves are required — a public identifier alone is not a\n * credential, so partial Basic is never synthesized.\n * - Else if `authToken` is given (or read from `APO_AUTH_TOKEN`), returns\n * `Bearer <token>`. (Covers short-lived task-run/attempt tokens and\n * secret-bearing legacy API keys.)\n * - Else returns an empty object (unauthenticated).\n *\n * Mirrors Python's `_build_auth_headers` in apo-otel-python. The resolution\n * core lives in `apo-auth.ts`, shared with the agent-task entry's env-only\n * variant of this builder.\n */\nexport function buildApoAuthHeaders(\n publicKey?: string,\n secretKey?: string,\n authToken?: string,\n): Record<string, string> {\n return resolveApoAuthHeaders(publicKey, secretKey, authToken);\n}\n\n/**\n * Resolve telemetry config: explicit kwarg wins, else env var, else default.\n * Mirrors Python's `_resolve_standalone_configuration`.\n */\nfunction resolveTelemetryConfig(options: ConfigureApoTelemetryOptions): {\n endpoint: string;\n headers: Record<string, string>;\n project: string | undefined;\n serviceName: string;\n} {\n const endpoint = options.endpoint\n ?? process.env.APO_OTLP_ENDPOINT\n ?? DEFAULT_APO_OTLP_ENDPOINT;\n const project = options.project ?? process.env.APO_PROJECT;\n const serviceName = options.serviceName ?? \"apo-agent\";\n const headers = options.headers\n ?? buildApoAuthHeaders(options.publicKey, options.secretKey, options.authToken);\n return { endpoint, headers, project, serviceName };\n}\n\n/**\n * Set the GenAI content-capture env var so instrumentation libs emit prompt\n * and completion text into spans. Uses `setdefault`-equivalent semantics: an\n * explicit user setting is never overridden. Mirrors Python's\n * `_configure_instrumentation_environment`.\n */\nfunction configureInstrumentationEnvironment(): void {\n if (!process.env.OTEL_INSTRUMENTATION_GENAI_CAPTURE_MESSAGE_CONTENT) {\n process.env.OTEL_INSTRUMENTATION_GENAI_CAPTURE_MESSAGE_CONTENT = \"span_only\";\n }\n}\n\n// ── configureApoTelemetry ────────────────────────────────────────────────\n\n/**\n * Configure apo's OpenTelemetry tracing.\n *\n * Builds apo's trace export from official OTel components: a real\n * {@link Resource} (service.name, service.version, deployment.environment)\n * and the official {@link OTLPTraceExporter} (standard OTLP/HTTP, no custom\n * wire format). This is the explicit standalone bootstrap. Host applications\n * that already own a provider should use `createApoSpanProcessor()` instead.\n *\n * @example\n * ```ts\n * import { configureApoTelemetry } from \"@apo-ai/sdk/otel\";\n *\n * const apo = await configureApoTelemetry({\n * takeOwnership: true,\n * endpoint: \"https://apo.example.com/api/public/otel/v1/traces\",\n * serviceName: \"my-agent\",\n * headers: { Authorization: `Basic ${btoa(\"pk-apo-...:sk-apo-...\")}` },\n * });\n *\n * const result = await withApoTrace({ name: \"chat\" }, apo.tracer, async (span) => {\n * span.setAttribute(\"gen_ai.request.model\", \"gpt-4o\");\n * // ... do work ...\n * return \"done\";\n * });\n *\n * await apo.shutdown();\n * ```\n */\nexport async function configureApoTelemetry(\n options: ConfigureApoTelemetryOptions,\n): Promise<ApoTelemetryHandle> {\n if (options.takeOwnership !== true) {\n throw new TypeError(\"standalone bootstrap requires takeOwnership: true\");\n }\n if (options.provider !== undefined) {\n throw new TypeError(\n \"A host provider cannot be mutated after construction in OTel JS 2.x; \" +\n \"construct it with createApoSpanProcessor() instead.\",\n );\n }\n\n // The standalone bootstrap owns this process's OTel lifecycle, so it also\n // owns the diag channel: OTel JS swallows export failures (retry\n // exhaustion, dropped spans) into a diag logger nobody sets by default —\n // warn-and-above to stderr keeps that from being silent without the\n // info-level noise.\n diag.setLogger(new DiagConsoleLogger(), DiagLogLevel.WARN);\n\n // Resolve config from kwargs → env vars → defaults (mirror apo-otel-python).\n const { endpoint, headers, project, serviceName } = resolveTelemetryConfig(options);\n\n // Enable prompt/completion content capture for instrumentation libs.\n configureInstrumentationEnvironment();\n\n // Set up the async context manager so context.active() carries the active\n // span. Concurrent apo handles share one owned manager; a host-owned manager\n // is used as-is and is never disabled by apo.\n const releaseContext = acquireContextManager();\n\n // Build the official OTel Resource with standard semantic attributes.\n const resourceAttrs: Record<string, string> = {\n [SEMRESATTRS_SERVICE_NAME]: serviceName,\n };\n if (project) resourceAttrs[SEMRESATTRS_SERVICE_NAMESPACE] = project;\n if (options.serviceVersion) resourceAttrs[SEMRESATTRS_SERVICE_VERSION] = options.serviceVersion;\n if (options.environment) resourceAttrs[SEMRESATTRS_DEPLOYMENT_ENVIRONMENT] = options.environment;\n const resource = resourceFromAttributes(resourceAttrs);\n\n // If an ApoSpanProcessor was registered via registerApoTracing(), add it to\n // this provider so GenAI spans from SDKs (Vercel AI, OpenAI, etc.) are fed\n // into the projection-tee for local assertions AND exported via OTLP.\n // It must run BEFORE the exporting processor: it enriches spans with\n // serving-identity attributes in onEnd, and enrichment after the export\n // read is lost.\n const apoProcessor = getRegisteredApoProcessor();\n\n // Official OTLP/HTTP trace exporter — standard OTLP wire format, no custom\n // serializer. Headers carry the auth the apo receiver validates.\n // The standalone path owns this provider. Host-owned providers compose with\n // createApoSpanProcessor() at their own construction boundary.\n const spanProcessors: SpanProcessor[] = [\n ...(apoProcessor\n ? [apoProcessor as unknown as SpanProcessor]\n : []),\n createApoSpanProcessor({\n endpoint,\n headers,\n processor: options.processor,\n scheduledDelayMillis: options.scheduledDelayMillis,\n }),\n ];\n\n const provider = new BasicTracerProvider({\n resource,\n spanProcessors,\n });\n\n const tracer = provider.getTracer(\"apo-sdk\", \"0.2.0\");\n\n // Register globally only when explicitly requested AND no host provider was\n // supplied AND no global provider is registered yet. apo never silently\n // replaces a host/global provider.\n let registeredGlobal = false;\n if (options.registerGlobal) {\n registeredGlobal = trace.setGlobalTracerProvider(provider);\n }\n\n let stopped = false;\n\n return {\n tracer,\n provider,\n async forceFlush() {\n await provider.forceFlush();\n },\n async shutdown() {\n if (stopped) return;\n stopped = true;\n // Force-flush + shut down the provider, but bound the whole teardown so\n // an unreachable export endpoint (e.g. no backend in unit tests) never\n // hangs the process. The span processors/exporters swallow their own\n // transport errors; this guards against the wait itself.\n await Promise.race([\n (async () => {\n await provider.forceFlush().catch(() => undefined);\n await provider.shutdown().catch(() => undefined);\n })(),\n new Promise<void>((resolve) => setTimeout(resolve, 2000)),\n ]);\n // Reset global state so a later configuration starts clean.\n if (registeredGlobal) {\n trace.disable();\n }\n releaseContext();\n },\n };\n}\n\nfunction acquireContextManager(): () => void {\n if (ownedContext !== null) {\n ownedContext.references += 1;\n const acquired = ownedContext;\n return () => releaseOwnedContext(acquired);\n }\n\n const manager = new AsyncHooksContextManager().enable();\n if (!context.setGlobalContextManager(manager)) {\n manager.disable();\n return () => undefined;\n }\n\n const acquired: OwnedContextState = { manager, references: 1 };\n ownedContext = acquired;\n return () => releaseOwnedContext(acquired);\n}\n\nfunction releaseOwnedContext(acquired: OwnedContextState): void {\n if (ownedContext !== acquired) return;\n acquired.references -= 1;\n if (acquired.references > 0) return;\n context.disable();\n ownedContext = null;\n}\n\n// ── withApoTrace ─────────────────────────────────────────────────────────\n\n/**\n * Run a function inside an OTel span.\n *\n * Creates a span using the active OTel context (standard context propagation,\n * not apo-specific span IDs passed by hand). The span automatically ends when\n * the function returns or throws.\n *\n * @example\n * ```ts\n * const result = await withApoTrace(\n * { name: \"agent-turn\", observationType: \"AGENT\" },\n * apo.tracer,\n * async (span) => {\n * span.setAttribute(\"gen_ai.request.model\", \"gpt-4o\");\n * return await llm.complete(\"hello\");\n * },\n * );\n * ```\n */\nexport async function withApoTrace<T>(\n options: ApoTraceOptions,\n tracer: Tracer,\n fn: (span: Span) => Promise<T>,\n): Promise<T> {\n const spanOptions: SpanOptions = {};\n if (options.attributes) {\n spanOptions.attributes = options.attributes as Record<string, string | number | boolean>;\n }\n\n // Use startActiveSpan which creates the span AND activates it in context,\n // so nested withApoTrace calls automatically inherit it as parent.\n return new Promise<T>((resolve, reject) => {\n tracer.startActiveSpan(options.name, spanOptions, async (span: Span) => {\n // Set apo vendor attributes\n if (options.observationType) {\n span.setAttribute(\"apo.observation.type\", options.observationType);\n }\n\n try {\n const result = await fn(span);\n span.end();\n resolve(result);\n } catch (err) {\n span.recordException(err as Error);\n span.setStatus({ code: 2, message: err instanceof Error ? err.message : String(err) });\n span.end();\n reject(err);\n }\n });\n });\n}\n\n// ── helper functions ─────────────────────────────────────────────────────\n\n/**\n * Trace a tool execution as an OTel span.\n *\n * Creates a span with ``gen_ai.tool.name``, ``gen_ai.tool.call.arguments``,\n * and ``gen_ai.tool.call.result`` attributes so the backend normalizer\n * classifies it as a TOOL observation.\n */\nexport async function traceTool<T>(\n tracer: Tracer,\n name: string,\n params: Record<string, unknown>,\n fn: () => Promise<T>,\n): Promise<T> {\n return withApoTrace(\n {\n name: `tool ${name}`,\n observationType: \"TOOL\",\n attributes: {\n \"gen_ai.tool.name\": name,\n \"gen_ai.tool.call.arguments\": JSON.stringify(params),\n },\n },\n tracer,\n async (span) => {\n const result = await fn();\n span.setAttribute(\"gen_ai.tool.call.result\", JSON.stringify(result));\n return result;\n },\n );\n}\n\n/**\n * Trace an agent invocation as an OTel span.\n */\nexport async function traceAgent<T>(\n tracer: Tracer,\n name: string,\n fn: () => Promise<T>,\n): Promise<T> {\n return withApoTrace(\n { name, observationType: \"AGENT\" },\n tracer,\n async () => fn(),\n );\n}\n\n/**\n * Trace a chain/step as an OTel span.\n */\nexport async function traceChain<T>(\n tracer: Tracer,\n name: string,\n fn: () => Promise<T>,\n): Promise<T> {\n return withApoTrace(\n { name, observationType: \"CHAIN\" },\n tracer,\n async () => fn(),\n );\n}\n\n/**\n * Trace a retrieval as an OTel span.\n */\nexport async function traceRetriever<T>(\n tracer: Tracer,\n query: string,\n fn: () => Promise<T>,\n): Promise<T> {\n return withApoTrace(\n { name: \"retrieve\", observationType: \"RETRIEVER\", attributes: { query } },\n tracer,\n async () => fn(),\n );\n}\n\n// ── Score API ─────────\n\nexport interface ScoreOptions {\n name: string;\n value: number | string | boolean;\n dataType?: \"NUMERIC\" | \"CATEGORICAL\" | \"BOOLEAN\";\n source?: \"API\" | \"EVAL\";\n comment?: string;\n observationId?: string;\n}\n\n/**\n * Record a score for a trace (or a specific observation within a trace).\n *\n * \"A Score is an apo domain record attached to a Trace or\n * Observation. It is not encoded as a fake span.\"\n *\n * This calls the native score API (`POST /api/v1/traces/{id}/scores` or\n * `POST /api/v1/observations/{id}/scores`) instead of creating a sentinel\n * span. The score is stored as a `RunMetricDB` or `CallMetricDB` row.\n *\n * @example\n * ```ts\n * await score({\n * traceId: \"abc123...\",\n * name: \"helpfulness\",\n * value: 0.85,\n * dataType: \"NUMERIC\",\n * source: \"EVAL\",\n * }, { endpoint: \"http://localhost:8000\", headers: authHeaders });\n * ```\n */\nexport async function score(\n params: ScoreOptions & { traceId: string },\n config: { endpoint: string; headers: Record<string, string> },\n): Promise<void> {\n const url = params.observationId\n ? `${config.endpoint}/api/v1/observations/${params.observationId}/scores`\n : `${config.endpoint}/api/v1/traces/${params.traceId}/scores`;\n\n const body: Record<string, unknown> = {\n name: params.name,\n value: params.value,\n data_type: params.dataType ?? \"NUMERIC\",\n source: params.source ?? \"API\",\n };\n if (params.comment) body.comment = params.comment;\n\n try {\n const response = await fetch(url, {\n method: \"POST\",\n headers: { \"Content-Type\": \"application/json\", ...config.headers },\n body: JSON.stringify(body),\n });\n if (!response.ok) {\n const text = await response.text().catch(() => \"\");\n console.error(`[apo score] HTTP ${response.status}: ${text.slice(0, 200)}`);\n }\n } catch (err) {\n console.error(\"[apo score] failed:\", err);\n }\n}\n\n// ── W3C trace context propagation ───────────────\n\n/**\n * Inject the current span's trace context into a headers dict for outbound\n * HTTP/queue calls. Produces a standard W3C `traceparent` header so the\n * downstream service can extract it and create a child span in the same trace.\n *\n * Delegates to OTel's official `W3CTraceContextPropagator`, which also emits\n * `tracestate` and handles the spec's edge cases.\n *\n * @example\n * ```ts\n * const headers = injectTraceparent();\n * await fetch(\"http://downstream-service/api\", { headers });\n * ```\n */\nexport function injectTraceparent(): Record<string, string> {\n const carrier: Record<string, string> = {};\n w3cPropagator.inject(context.active(), carrier, defaultTextMapSetter);\n return carrier;\n}\n\n/**\n * Extract a W3C trace context from inbound headers and run a function as a\n * child span within that trace. If no `traceparent` header is present (or it\n * is malformed), a new root span is created.\n *\n * Transport parsing is delegated to OTel's `W3CTraceContextPropagator`; the\n * span creation on top of the extracted remote parent is apo-specific and\n * kept here.\n *\n * @example\n * ```ts\n * // In a downstream HTTP handler:\n * app.post(\"/api\", (req, res) => {\n * const result = await extractTraceparent(req.headers, tracer, async (span) => {\n * return await doWork();\n * });\n * res.json(result);\n * });\n * ```\n */\nexport async function extractTraceparent<T>(\n headers: Record<string, string>,\n tracer: Tracer,\n fn: (span: Span) => Promise<T>,\n): Promise<T> {\n // Extract the upstream context. The propagator handles invalid/missing\n // traceparent by returning the input context unchanged (no extracted span).\n const extracted = w3cPropagator.extract(context.active(), headers, defaultTextMapGetter);\n const parent = trace.getSpan(extracted);\n if (!parent) {\n // No upstream context → new root span\n return withApoTrace({ name: \"inbound\" }, tracer, fn);\n }\n\n // Create a child span in the extracted context (shares the upstream traceId).\n return new Promise<T>((resolve, reject) => {\n context.with(extracted, () => {\n tracer.startActiveSpan(\"inbound\", async (span: Span) => {\n try {\n const result = await fn(span);\n span.end();\n resolve(result);\n } catch (err) {\n span.recordException(err as Error);\n span.end();\n reject(err);\n }\n });\n });\n });\n}\n","/**\n * Per-run context propagation via Node's AsyncLocalStorage.\n *\n * The OTel {@link ApoSpanProcessor} is global — it doesn't know which run a\n * span belongs to. This module solves that: {@link withApoRun} establishes\n * the run's trace context on AsyncLocalStorage, and any code called during\n * the run (including the processor's `onStart`/`onEnd`) can read it via\n * {@link getActiveApoRun}.\n *\n * This is the same propagation mechanism OTel's own\n * `AsyncLocalStorageContextManager` uses under the hood.\n *\n * @module\n */\n\nimport { AsyncLocalStorage } from \"node:async_hooks\";\nimport type { AgentTaskTraceContext } from \"../tracing.ts\";\n\nexport interface ApoRunContext {\n /** The tee'd trace context for this run. */\n trace: AgentTaskTraceContext;\n /** The parent span id (the run's root span or the current turn span). */\n parentSpanId?: string;\n /** The task id, surfaced in span metadata. */\n taskId?: string;\n /** The current turn number, surfaced in span metadata. */\n turnNumber?: number;\n}\n\nconst runStorage = new AsyncLocalStorage<ApoRunContext>();\n\n/**\n * Run `fn` with `ctx` as the active apo run context. Any code inside `fn`\n * (including across `await` boundaries) can call {@link getActiveApoRun}\n * to retrieve it.\n */\nexport function withApoRun<T>(ctx: ApoRunContext, fn: () => Promise<T>): Promise<T> {\n return runStorage.run(ctx, fn);\n}\n\n/**\n * Run `fn` synchronously with `ctx` as the active apo run context.\n */\nexport function withApoRunSync<T>(ctx: ApoRunContext, fn: () => T): T {\n return runStorage.run(ctx, fn);\n}\n\n/**\n * Get the active run context, or `undefined` if called outside a run.\n *\n * This is what the {@link ApoSpanProcessor} calls in `onStart`/`onEnd` to\n * know which run a span belongs to.\n */\nexport function getActiveApoRun(): ApoRunContext | undefined {\n return runStorage.getStore();\n}\n","/**\n * Shared span-translation helpers for the tracing integrations.\n *\n * Maps OpenTelemetry spans from both the Vercel AI SDK (`ai.*` attributes)\n * and the standard GenAI semantic conventions (`gen_ai.*` attributes) to\n * apo's observation types (`TOOL`, `GENERATION`, `AGENT`).\n *\n * Used by:\n * - {@link ApoSpanProcessor} (the OTel-native path)\n * - `createApoTracer` (the per-turn AI SDK tracer)\n *\n * @module\n */\n\n// ── shared utility functions ─────────────────────────────────────────────\n\nimport { extractTokenCounts } from \"./token-usage.ts\";\n\n/** Parse a JSON string, returning the original value if parsing fails. */\nexport function safeParse(raw: unknown): unknown {\n if (typeof raw !== \"string\") return raw;\n try {\n return JSON.parse(raw);\n } catch {\n return raw;\n }\n}\n\n/** High-resolution monotonic timestamp in milliseconds. */\nexport function monotonicNowMs(): number {\n if (typeof performance !== \"undefined\" && typeof performance.now === \"function\") {\n return performance.now();\n }\n return Date.now();\n}\n\n/** Round to 3 decimal places (for latency precision). */\nexport function round3(n: number): number {\n return Math.round(n * 1000) / 1000;\n}\n\n// ── the translation contract ─────────────────────────────────────────────\n\n/**\n * apo observation types the projection recognizes. Only spans with one\n * of these types become events that `t.*` assertions read.\n */\nexport type ApoObservationType = \"TOOL\" | \"GENERATION\" | \"AGENT\";\n\n/**\n * The result of translating an OTel span to apo's format.\n * `null` means the span is not load-bearing (housekeeping, internal, etc.)\n * and should be ignored.\n */\nexport interface TranslatedSpan {\n observationType: ApoObservationType;\n stepName: string;\n /** Model name (for GENERATION spans). */\n model?: string;\n /** Observed serving identity the emitter declared through the AI SDK's\n * telemetry metadata (never guessed here; absent fields stay unknown). */\n serving?: {\n apiProvider?: string;\n servingHost?: string;\n route?: string;\n serviceTier?: string;\n };\n /** Tool call input (for TOOL spans). */\n input?: Record<string, unknown>;\n /** Tool call result (set on end, not start). */\n output?: unknown;\n /** Generation text (for GENERATION spans). */\n text?: string;\n promptTokens?: number;\n completionTokens?: number;\n /** Reasoning/thinking tokens, when the provider reports the dimension. */\n reasoningTokens?: number;\n error?: boolean;\n errorMessage?: string;\n}\n\n/**\n * Serving identity from the AI SDK's telemetry metadata. An app that knows\n * who it calls declares it once per call site:\n * `experimental_telemetry: { metadata: { \"apo.llm.api_provider\": \"openrouter\" } }`\n * and the SDK flattens it onto every span as\n * `ai.telemetry.metadata.apo.llm.api_provider`. String values only; every\n * other shape stays unknown.\n */\nexport function servingFromTelemetryMetadata(\n attributes: Record<string, unknown>,\n): TranslatedSpan[\"serving\"] {\n const prefix = \"ai.telemetry.metadata.apo.llm.\";\n const keys = [\"api_provider\", \"serving_host\", \"route\", \"service_tier\"] as const;\n let serving: TranslatedSpan[\"serving\"];\n for (const key of keys) {\n const value = attributes[prefix + key];\n if (typeof value === \"string\" && value.trim() !== \"\") {\n serving ??= {};\n if (key === \"api_provider\") serving.apiProvider = value;\n else if (key === \"serving_host\") serving.servingHost = value;\n else if (key === \"route\") serving.route = value;\n else serving.serviceTier = value;\n }\n }\n return serving;\n}\n\n/**\n * An OTel span's attributes and status, in a shape that doesn't require\n * importing `@opentelemetry/api` at the type level.\n */\nexport interface OtelSpanData {\n attributes: Record<string, unknown>;\n status: { code: number; message?: string };\n}\n\n/**\n * Translate an OTel span to apo's observation format.\n *\n * Handles both attribute conventions:\n * - **AI SDK** (`ai.*`): `ai.toolCall.*`, `ai.generateText`, `ai.streamText`\n * - **GenAI standard** (`gen_ai.*`): `gen_ai.tool.*`, `gen_ai.operation.name`\n *\n * Returns `null` for spans that aren't load-bearing (internal housekeeping,\n * non-GenAI operations, etc.).\n */\nexport function translateOtelSpan(\n spanName: string,\n data: OtelSpanData,\n): TranslatedSpan | null {\n const { attributes, status } = data;\n const isError = status.code === 2;\n const errorMessage = status.message;\n\n // ── AI SDK convention (ai.*) ────────────────────────────────────────\n\n if (spanName === \"ai.toolCall\") {\n return {\n observationType: \"TOOL\",\n stepName: String(attributes[\"ai.toolCall.name\"] ?? \"unknown\"),\n input: safeParse(attributes[\"ai.toolCall.args\"]) as Record<string, unknown>,\n output: safeParse(attributes[\"ai.toolCall.result\"]),\n error: isError,\n errorMessage,\n };\n }\n\n // The AI SDK emits a parent span (ai.generateText) and per-step child spans\n // (ai.generateText.doGenerate). The parent span carries:\n // - ai.model.id (at creation)\n // - ai.prompt.messages (the input, at creation)\n // - ai.response.text (the final assembled text, set at end)\n // - ai.usage.promptTokens/completionTokens (total usage, set at end)\n // The child doGenerate spans carry per-step data but NOT the final text.\n // We translate the parent span (which has the complete picture) and skip\n // the per-step child spans.\n if (\n spanName === \"ai.generateText\" ||\n spanName === \"ai.streamText\"\n ) {\n return {\n observationType: \"GENERATION\",\n stepName: \"agent.generate\",\n text: extractTextFromAttrs(attributes),\n model: String(\n attributes[\"gen_ai.request.model\"] ??\n attributes[\"ai.model.id\"] ??\n attributes[\"ai.response.model\"] ??\n \"unknown\",\n ),\n serving: servingFromTelemetryMetadata(attributes),\n ...extractTokenCounts(attributes),\n error: isError,\n errorMessage,\n };\n }\n\n // ── Standard OTel GenAI conventions (gen_ai.*) ─────────────────────\n\n const operation = attributes[\"gen_ai.operation.name\"];\n\n // Tool execution span\n if (\n operation === \"execute_tool\" ||\n spanName.startsWith(\"gen_ai.tool\") ||\n (spanName.includes(\"tool\") && attributes[\"gen_ai.tool.name\"])\n ) {\n return {\n observationType: \"TOOL\",\n stepName: String(attributes[\"gen_ai.tool.name\"] ?? \"unknown\"),\n input: safeParse(attributes[\"gen_ai.tool.call.arguments\"]) as Record<string, unknown>,\n output: safeParse(attributes[\"gen_ai.tool.call.result\"]),\n error: isError,\n errorMessage,\n };\n }\n\n // Agent invocation span\n if (operation === \"invoke_agent\") {\n return {\n observationType: \"AGENT\",\n stepName: String(attributes[\"gen_ai.agent.name\"] ?? \"agent\"),\n output: safeParse(attributes[\"gen_ai.tool.call.result\"]),\n error: isError,\n errorMessage,\n };\n }\n\n // Chat / generation span\n if (\n operation === \"chat\" ||\n operation === \"generate_content\" ||\n operation === \"text_completion\"\n ) {\n return {\n observationType: \"GENERATION\",\n stepName: \"agent.generate\",\n text: extractTextFromAttrs(attributes, \"gen_ai.\"),\n ...extractTokenCounts(attributes),\n error: isError,\n errorMessage,\n };\n }\n\n // ── Not a load-bearing span ────────────────────────────────────────\n\n return null;\n}\n\n/**\n * Extract the model's text response from span attributes.\n * Tries both `ai.*` and `gen_ai.*` response keys.\n */\nexport function extractTextFromAttrs(\n attrs: Record<string, unknown>,\n prefix?: string,\n): string | undefined {\n const candidates = prefix === \"gen_ai.\"\n ? [\"gen_ai.completion\", \"gen_ai.response.text\"]\n : prefix === \"ai.\"\n ? [\"ai.response.text\", \"ai.response.output\", \"ai.generateText.result\"]\n : [\n \"gen_ai.completion\",\n \"gen_ai.response.text\",\n \"ai.response.text\",\n \"ai.response.output\",\n \"ai.generateText.result\",\n ];\n\n for (const key of candidates) {\n const val = attrs[key];\n if (typeof val === \"string\" && val.length > 0) {\n const parsed = safeParse(val);\n return typeof parsed === \"string\" ? parsed : val;\n }\n }\n return undefined;\n}\n","/**\n * ApoSpanProcessor — an OpenTelemetry SpanProcessor that routes GenAI spans\n * into apo's Flow.\n *\n * This is the OTel-native tracing path. After calling {@link registerApoTracing},\n * any SDK that emits OTel spans (Vercel AI SDK, OpenAI Agents SDK, etc.)\n * will be traced automatically — no per-SDK wrapper needed.\n *\n * The processor is global, but run isolation is maintained via\n * {@link AsyncLocalStorage}: {@link getActiveApoRun} returns the current\n * run's trace context, so each span is routed to the correct run's trace.\n *\n * **Design: eager creation, deferred completion.** The apo span is created\n * at `onStart` (when initial attributes like tool name and model are\n * available). The span is ended at `onEnd` with the final attributes\n * (results, tokens, text). This gives correct ordering: parent spans\n * are created before children, matching the real call hierarchy.\n *\n * @module\n */\n\nimport { getActiveApoRun } from \"./run-context.ts\";\nimport {\n translateOtelSpan,\n servingFromTelemetryMetadata,\n round3,\n monotonicNowMs,\n type TranslatedSpan,\n} from \"./otel-translate.ts\";\n\n// ── minimal OTel type shapes (no runtime dependency on @opentelemetry/api) ──\n\ninterface OTelSpanContext {\n spanId: string;\n traceId: string;\n traceFlags: number;\n}\n\ninterface OTelSpanStatus {\n code: number;\n message?: string;\n}\n\ninterface OTelReadableSpan {\n name: string;\n spanContext(): OTelSpanContext;\n parentSpanContext?: OTelSpanContext;\n attributes: Record<string, unknown>;\n status: OTelSpanStatus;\n duration: [number, number];\n startTime: [number, number];\n ended: boolean;\n}\n\ninterface OTelSpan extends OTelReadableSpan {\n setAttribute(key: string, value: unknown): this;\n setAttributes(attributes: Record<string, unknown>): this;\n setStatus(status: OTelSpanStatus): this;\n recordException(exception: unknown): void;\n updateName(name: string): this;\n end(endTime?: unknown): void;\n isRecording(): boolean;\n}\n\ninterface OTelContext {\n getValue(key: symbol): unknown;\n setValue(key: symbol, value: unknown): OTelContext;\n deleteValue(key: symbol): OTelContext;\n}\n\ninterface OTelSpanProcessorLifecycle {\n forceFlush(): Promise<void>;\n shutdown(): Promise<void>;\n}\n\ninterface TrackedSpan {\n apoSpanId: string;\n otelSpanName: string;\n startedAt: number;\n}\n\n/**\n * Do not remove the parent-stamping built on this helper: the Vercel AI\n * SDK reports the serving model only on per-step doGenerate children,\n * which are not load-bearing, so re-attaching their report to the parent\n * is the only way the serving model reaches the exported span — the\n * backend reads gen_ai.response.model for pricing and model-drift\n * detection on OTel-native (non-wrapper) traces.\n */\nfunction servedModelFrom(\n attributes: Record<string, unknown>,\n): string | undefined {\n for (const key of [\"ai.response.model\", \"gen_ai.response.model\"]) {\n const value = attributes[key];\n if (typeof value === \"string\" && value) return value;\n }\n return undefined;\n}\n\n/**\n * A SpanProcessor that translates GenAI OTel spans into apo observations.\n *\n * Spans are created at `onStart` (eager) and completed at `onEnd`\n * (deferred). This gives correct parent/child ordering — parent spans\n * are created before their children, matching the real call hierarchy.\n *\n * If a span isn't load-bearing (not a GenAI tool/generation span), no\n * apo span is created.\n */\nexport class ApoSpanProcessor implements OTelSpanProcessorLifecycle {\n private spanMap = new Map<string, TrackedSpan>();\n private servedModelByParent = new Map<string, string>();\n private servingByParent = new Map<\n string,\n {\n apiProvider?: string;\n servingHost?: string;\n route?: string;\n serviceTier?: string;\n }\n >();\n\n onStart(span: OTelSpan, _parentContext: OTelContext): void {\n const run = getActiveApoRun();\n if (!run) return;\n\n // At onStart the AI SDK has already set the initial attributes\n // (ai.toolCall.name, ai.toolCall.args for tool spans; ai.model.id\n // for generation spans), so translation can happen now.\n const translated = translateOtelSpan(span.name, {\n attributes: span.attributes ?? {},\n status: { code: 0 }, // no error at start\n });\n if (!translated) return; // not load-bearing — skip\n\n const parentOtelId = span.parentSpanContext?.spanId;\n const parentTracked = parentOtelId ? this.spanMap.get(parentOtelId) : undefined;\n const parentApoId = parentTracked?.apoSpanId || run.parentSpanId || run.trace.rootSpanId;\n\n const apoSpanId = run.trace.createSpan({\n task_id: run.taskId ?? \"otel-trace\",\n parent_call_id: parentApoId,\n step_name: translated.stepName,\n model: translated.model ?? translated.stepName,\n observation_type: translated.observationType,\n ...(translated.serving?.apiProvider\n ? { api_provider: translated.serving.apiProvider }\n : {}),\n ...(translated.serving?.servingHost\n ? { serving_host: translated.serving.servingHost }\n : {}),\n ...(translated.serving?.route ? { route: translated.serving.route } : {}),\n ...(translated.input ? { input: translated.input } : {}),\n metadata: {\n ...(run.taskId ? { taskId: run.taskId } : {}),\n ...(run.turnNumber !== undefined ? { turnNumber: run.turnNumber } : {}),\n source: \"otel-processor\",\n },\n });\n\n this.spanMap.set(span.spanContext().spanId, {\n apoSpanId,\n otelSpanName: span.name,\n startedAt: monotonicNowMs(),\n });\n }\n\n onEnd(span: OTelReadableSpan): void {\n const otelId = span.spanContext().spanId;\n\n // A per-step child reporting its serving model is the parent\n // generation's served model — remember it for the parent's end.\n const parentOtelId = span.parentSpanContext?.spanId;\n const served = servedModelFrom(span.attributes ?? {});\n if (parentOtelId && served) {\n this.servedModelByParent.set(parentOtelId, served);\n }\n // Same inheritance for serving identity: the AI SDK puts base telemetry\n // attributes (including the app-declared apo.llm.* metadata) on the\n // doGenerate/doStream children, not the wrapper the processor tracks.\n const declaredServing = servingFromTelemetryMetadata(span.attributes ?? {});\n if (parentOtelId && declaredServing) {\n this.servingByParent.set(parentOtelId, declaredServing);\n }\n\n // Own report first, then what a child reported for this span. A span\n // ends once, so claim its slot here: the processor is a singleton that\n // outlives runs, and an entry left for a span that ended untracked or\n // outside its run's context would never be removed.\n const servedForThis = served ?? this.servedModelByParent.get(otelId);\n this.servedModelByParent.delete(otelId);\n const servingForThis =\n declaredServing ?? this.servingByParent.get(otelId);\n this.servingByParent.delete(otelId);\n\n const tracked = this.spanMap.get(otelId);\n if (!tracked) return; // wasn't tracked (not load-bearing or outside run)\n this.spanMap.delete(otelId);\n\n const run = getActiveApoRun();\n if (!run) return;\n\n // Re-translate with final attributes (results, text, tokens set during execution)\n const translated = translateOtelSpan(tracked.otelSpanName, {\n attributes: span.attributes ?? {},\n status: span.status ?? { code: 0 },\n });\n\n const latency = round3(monotonicNowMs() - tracked.startedAt);\n\n // The backend projects from the RAW exported span, so identity must\n // live on this span itself, not only the in-process apo view. Setting\n // attributes in onEnd is the sanctioned processor hook to enrich a\n // span before export.\n if (servingForThis) {\n const raw = span as OTelReadableSpan & {\n setAttribute?: (key: string, value: string) => void;\n };\n if (servingForThis.apiProvider) {\n raw.setAttribute?.(\"apo.llm.api_provider\", servingForThis.apiProvider);\n }\n if (servingForThis.servingHost) {\n raw.setAttribute?.(\"apo.llm.serving_host\", servingForThis.servingHost);\n }\n if (servingForThis.route) {\n raw.setAttribute?.(\"apo.llm.route\", servingForThis.route);\n }\n if (servingForThis.serviceTier) {\n raw.setAttribute?.(\"apo.llm.service_tier\", servingForThis.serviceTier);\n }\n // A wrapper generation is one logical operation; its children are\n // the physical requests. The backend's rollups read this marker.\n if (tracked.otelSpanName === \"ai.generateText\" || tracked.otelSpanName === \"ai.streamText\") {\n raw.setAttribute?.(\"apo.llm.request_kind\", \"aggregate\");\n }\n }\n\n run.trace.endSpan(tracked.apoSpanId, {\n latency_ms: latency,\n ...buildEndParams(translated),\n ...(servedForThis ? { served_model: servedForThis } : {}),\n ...(servingForThis?.apiProvider\n ? { api_provider: servingForThis.apiProvider }\n : {}),\n ...(servingForThis?.servingHost\n ? { serving_host: servingForThis.servingHost }\n : {}),\n });\n }\n\n forceFlush(): Promise<void> {\n return Promise.resolve();\n }\n\n shutdown(): Promise<void> {\n this.spanMap.clear();\n this.servedModelByParent.clear();\n this.servingByParent.clear();\n return Promise.resolve();\n }\n\n /** Clear all state — for testing. */\n reset(): void {\n this.spanMap.clear();\n this.servedModelByParent.clear();\n this.servingByParent.clear();\n }\n}\n\nfunction buildEndParams(t: TranslatedSpan | null): Record<string, unknown> {\n if (!t) return {};\n\n const params: Record<string, unknown> = {};\n\n if (t.promptTokens !== undefined) params.prompt_tokens = t.promptTokens;\n if (t.completionTokens !== undefined) params.completion_tokens = t.completionTokens;\n if (t.reasoningTokens !== undefined) params.reasoning_tokens = t.reasoningTokens;\n\n if (t.observationType === \"GENERATION\") {\n if (t.text !== undefined) {\n params.output = { text: t.text };\n }\n } else if (t.observationType === \"TOOL\") {\n if (t.output !== undefined) {\n params.output =\n t.output && typeof t.output === \"object\"\n ? (t.output as Record<string, unknown>)\n : { value: t.output };\n }\n }\n\n if (t.error) {\n params.level = \"ERROR\";\n if (t.errorMessage) params.status_message = t.errorMessage;\n }\n\n return params;\n}\n","/**\n * Registration API for the OTel-native tracing integration.\n *\n * Call {@link registerApoTracing} once at startup (e.g., in `startSession`\n * or at module load). After this, any SDK that emits OTel spans during a run\n * will be traced automatically — no per-SDK wrapper needed.\n *\n * @example\n * ```ts\n * import { registerApoTracing } from \"@apo-ai/sdk/agent-task\";\n *\n * async startSession(ctx) {\n * registerApoTracing(); // one line, once\n * return { async sendUserTurn(turn) { ... } };\n * }\n * ```\n *\n * @module\n */\n\nimport { ApoSpanProcessor } from \"./otel-processor.ts\";\n\n// We use dynamic imports for @opentelemetry so the module is importable\n// without the packages installed. In practice they're transitive deps of `ai`.\n\nlet registered = false;\nlet currentProcessor: ApoSpanProcessor | null = null;\n\nexport interface RegisterApoTracingOptions {\n /**\n * Set as the global tracer provider (default: true).\n * Set to false if the host app has its own OTel setup and you want to\n * add apo's processor to an existing provider instead.\n */\n setGlobal?: boolean;\n}\n\n/**\n * Register apo's OTel SpanProcessor. Idempotent — calling it more than\n * once is a no-op.\n *\n * After registration, any OTel span created during an apo run (inside\n * `withApoRun`) that matches the GenAI conventions (`ai.*` or `gen_ai.*`)\n * is automatically translated and fed into the run's Flow.\n */\nexport async function registerApoTracing(\n _options?: RegisterApoTracingOptions,\n): Promise<void> {\n if (registered) return;\n registered = true;\n\n // Create the processor but DON'T register our own provider. The processor\n // is picked up by configureApoTelemetry (via getRegisteredApoProcessor)\n // and added to the OTLP-exporting provider. This ensures GenAI spans from\n // SDKs (Vercel AI, OpenAI, etc.) reach BOTH the projection-tee (for local\n // assertions) AND the OTLP exporter (for the backend), all through a single\n // global provider — no competing providers.\n currentProcessor = new ApoSpanProcessor();\n}\n\n/**\n * Alias used by configureApoTelemetry to pick up the processor and add it\n * to the OTLP-exporting provider.\n */\nexport function getRegisteredApoProcessor(): ApoSpanProcessor | null {\n return currentProcessor;\n}\n\n/**\n * Reset registration state — for testing only.\n */\nexport function resetApoTracing(): void {\n registered = false;\n currentProcessor = null;\n}\n","/**\n * Credential resolution for apo backends — the one implementation every\n * exporter (the OTel trace client, transcript-replay capture, CLI import)\n * authenticates through. Dependency-free on purpose: both the otel and\n * agent-task entries import it without pulling each other's weight in.\n *\n * - ``APO_PUBLIC_KEY`` + ``APO_SECRET_KEY`` → Basic auth (two-key API key;\n * both halves required — a public identifier alone is not a credential)\n * - ``APO_AUTH_TOKEN`` → Bearer (attempt/service token)\n *\n * Explicit arguments win over the environment; when nothing resolves, the\n * caller decides whether unauthenticated (`{}`) or \"no headers\" (undefined)\n * is the right shape.\n */\n\nexport function resolveApoAuthHeaders(\n publicKey?: string,\n secretKey?: string,\n authToken?: string,\n): Record<string, string> {\n const pk = publicKey ?? process.env.APO_PUBLIC_KEY;\n const sk = secretKey ?? process.env.APO_SECRET_KEY;\n const token = authToken ?? process.env.APO_AUTH_TOKEN;\n if (pk && sk) {\n const credentials = typeof btoa === \"function\"\n ? btoa(`${pk}:${sk}`)\n : Buffer.from(`${pk}:${sk}`).toString(\"base64\");\n return { Authorization: `Basic ${credentials}` };\n }\n if (token) {\n return { Authorization: `Bearer ${token}` };\n }\n return {};\n}\n"],"mappings":"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","names":["acquired"]}
|
|
@@ -29,6 +29,9 @@ function emitGenerationAndTools(trace, genSpanId, genStartedAt, opts) {
|
|
|
29
29
|
reasoning_tokens: opts.reasoningTokens,
|
|
30
30
|
served_model: opts.servedModel,
|
|
31
31
|
...opts.servedProvider ? { provider: opts.servedProvider } : {},
|
|
32
|
+
// The response-reported host is serving-host evidence distinct from
|
|
33
|
+
// the API provider: gateways report who executed, not who was called.
|
|
34
|
+
...opts.servedProvider ? { serving_host: opts.servedProvider } : {},
|
|
32
35
|
...opts.servedRoute ? { route: opts.servedRoute } : {},
|
|
33
36
|
output: {
|
|
34
37
|
...opts.text !== void 0 ? { text: opts.text } : {},
|
|
@@ -121,6 +124,7 @@ function startGeneration(trace, opts) {
|
|
|
121
124
|
step_name: "agent.generate",
|
|
122
125
|
model: opts.model,
|
|
123
126
|
provider: opts.provider,
|
|
127
|
+
api_provider: opts.apiProvider,
|
|
124
128
|
route: opts.route,
|
|
125
129
|
observation_type: "GENERATION",
|
|
126
130
|
input: {
|
|
@@ -142,4 +146,4 @@ export {
|
|
|
142
146
|
servingFromResponse,
|
|
143
147
|
startGeneration
|
|
144
148
|
};
|
|
145
|
-
//# sourceMappingURL=chunk-
|
|
149
|
+
//# sourceMappingURL=chunk-KWIYQSWK.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../src/agent-task/integrations/span-helpers.ts"],"sourcesContent":["/**\n * Shared span-emission helpers for the tracing integrations.\n *\n * The OpenAI and Anthropic wrappers both need to:\n * 1. Create a GENERATION span before the LLM call\n * 2. End it with text/tokens/latency after the response\n * 3. Emit a TOOL span for each tool call in the response\n *\n * These helpers keep that logic in one place so the wrappers stay thin.\n *\n * @internal\n */\n\nimport type { AgentTaskTraceContext } from \"../tracing.ts\";\n\nfunction monotonicNowMs(): number {\n if (typeof performance !== \"undefined\" && typeof performance.now === \"function\") {\n return performance.now();\n }\n return Date.now();\n}\n\nfunction round3(n: number): number {\n return Math.round(n * 1000) / 1000;\n}\n\nfunction safeParse(raw: unknown): unknown {\n if (typeof raw !== \"string\") return raw;\n try {\n return JSON.parse(raw);\n } catch {\n return raw;\n }\n}\n\n/**\n * Emit a GENERATION span + TOOL spans for a completed LLM call.\n *\n * Both wrappers call this after the SDK returns the response. It:\n * 1. Ends the GENERATION span (created by {@link startGeneration}) with\n * text, token counts, and latency.\n * 2. Emits a TOOL span per tool call, so `t.calledTool(name)` works.\n */\nexport function emitGenerationAndTools(\n trace: AgentTaskTraceContext,\n genSpanId: string,\n genStartedAt: number,\n opts: {\n text?: string;\n promptTokens?: number;\n completionTokens?: number;\n /** Reasoning tokens, when the provider reports the dimension. */\n reasoningTokens?: number;\n /** The model that actually served the call (response `model` field). */\n servedModel?: string;\n /** Serving host learned from the response (gateway metadata/headers);\n * overrides the start-time baseURL guess when present. */\n servedProvider?: string;\n servedRoute?: string;\n toolCalls?: Array<{ name: string; input?: unknown }>;\n taskId?: string;\n turnNumber?: number;\n error?: { message: string };\n },\n): void {\n const latency = round3(monotonicNowMs() - genStartedAt);\n const isError = !!opts.error;\n\n // End the GENERATION span\n trace.endSpan(genSpanId, {\n latency_ms: latency,\n prompt_tokens: opts.promptTokens,\n completion_tokens: opts.completionTokens,\n reasoning_tokens: opts.reasoningTokens,\n served_model: opts.servedModel,\n ...(opts.servedProvider ? { provider: opts.servedProvider } : {}),\n // The response-reported host is serving-host evidence distinct from\n // the API provider: gateways report who executed, not who was called.\n ...(opts.servedProvider ? { serving_host: opts.servedProvider } : {}),\n ...(opts.servedRoute ? { route: opts.servedRoute } : {}),\n output: {\n ...(opts.text !== undefined ? { text: opts.text } : {}),\n ...(opts.error ? { error: opts.error.message } : {}),\n },\n ...(isError\n ? { level: \"ERROR\" as const, status_message: opts.error!.message }\n : {}),\n });\n\n // Emit TOOL spans (only on success — errors are captured on the GENERATION span)\n if (!isError && opts.toolCalls) {\n for (const tc of opts.toolCalls) {\n const toolSpanId = trace.createSpan({\n task_id: opts.taskId ?? \"trace\",\n parent_call_id: genSpanId,\n step_name: tc.name,\n observation_type: \"TOOL\",\n ...(tc.input !== undefined\n ? {\n input:\n tc.input && typeof tc.input === \"object\"\n ? (tc.input as Record<string, unknown>)\n : { value: tc.input },\n }\n : {}),\n metadata: { toolName: tc.name },\n });\n trace.endSpan(toolSpanId, {});\n }\n }\n}\n\n/**\n * Resolve the serving host a client actually talks to, from its baseURL.\n *\n * The vendor default (no baseURL) means the SDK's own endpoint. A custom\n * baseURL is the serving host itself — for a known gateway the provider is\n * its name; for anything else the registered domain is the provider and the\n * full host the route, which is exactly the split the backend stores\n * (`gen_ai.provider.name` / `apo.llm.route`, issue #307). Local/private\n * hosts are proxies in front of an unknown backend — report nothing rather\n * than a wrong host.\n */\nexport function servingHostFromBaseURL(\n baseURL: unknown,\n defaultVendor?: string,\n): { provider?: string; route?: string } {\n if (typeof baseURL !== \"string\" || baseURL.trim() === \"\") {\n return defaultVendor ? { provider: defaultVendor } : {};\n }\n let host: string;\n try {\n host = new URL(baseURL).hostname;\n } catch {\n return defaultVendor ? { provider: defaultVendor } : {};\n }\n if (\n host === \"localhost\" ||\n /^(127\\.|0\\.0.0\\.0|10\\.|192\\.168\\.|172\\.(1[6-9]|2\\d|3[01])\\.)/.test(host)\n ) {\n // A private host is a gateway in front of an unknown backend. An\n // operator can name it once via APO_SERVING_GATEWAY (e.g. \"litellm\")\n // instead of every run reporting \"unknown\".\n const named = process.env.APO_SERVING_GATEWAY?.trim();\n return named ? { provider: named } : {};\n }\n const known: Record<string, string> = {\n \"api.openai.com\": \"openai\",\n \"api.anthropic.com\": \"anthropic\",\n \"openrouter.ai\": \"openrouter\",\n \"api.fireworks.ai\": \"fireworks\",\n \"api.groq.com\": \"groq\",\n \"api.cerebras.ai\": \"cerebras\",\n \"api.deepseek.com\": \"deepseek\",\n \"api.mistral.ai\": \"mistral\",\n \"api.x.ai\": \"xai\",\n \"api.together.xyz\": \"together\",\n \"generativelanguage.googleapis.com\": \"google\",\n };\n const provider = known[host];\n if (provider) return { provider };\n const labels = host.split(\".\");\n if (labels.length < 2) return {};\n return { provider: labels.slice(-2).join(\".\"), route: host };\n}\n\n/**\n * Derive the serving host a gateway reports about itself, from the response.\n *\n * Gateways in front of many backends know exactly which one served a call;\n * the baseURL only ever names the gateway. Priority:\n * 1. OpenRouter's opt-in `openrouter_metadata.endpoints.available[selected]`\n * (send `X-OpenRouter-Metadata: enabled` to receive it).\n * 2. LiteLLM's response headers: `x-litellm-model-id` (the deployment)\n * and `x-litellm-model-api-base` (the upstream actually called).\n * Returns {} when the gateway reports nothing — unknown stays unknown.\n */\nexport function servingFromResponse(\n response: {\n openrouter_metadata?: {\n endpoints?: {\n available?: Array<{ provider?: unknown; selected?: unknown }>;\n };\n };\n },\n headers?: { get(name: string): string | null } | null,\n): { provider?: string; route?: string } {\n const endpoints = response?.openrouter_metadata?.endpoints?.available;\n if (Array.isArray(endpoints)) {\n const selected = endpoints.find(\n (e) => e && typeof e === \"object\" && e.selected === true,\n );\n const provider =\n selected && typeof (selected as { provider?: unknown }).provider === \"string\"\n ? (selected as { provider: string }).provider\n : undefined;\n if (provider) {\n return { provider: \"openrouter\", route: `openrouter:${provider}` };\n }\n }\n const litellmId = headers?.get?.(\"x-litellm-model-id\");\n if (typeof litellmId === \"string\" && litellmId) {\n const upstream = headers?.get?.(\"x-litellm-model-api-base\");\n const host =\n typeof upstream === \"string\" && upstream\n ? safeUrlHost(upstream)\n : undefined;\n return {\n provider: \"litellm\",\n route: host ? `litellm:${litellmId}@${host}` : `litellm:${litellmId}`,\n };\n }\n return {};\n}\n\nfunction safeUrlHost(url: string): string | undefined {\n try {\n return new URL(url).hostname;\n } catch {\n return undefined;\n }\n}\n\n/**\n * Create a GENERATION span before the LLM call.\n * Returns `{ spanId, startedAt }` to pass to {@link emitGenerationAndTools}.\n */\nexport function startGeneration(\n trace: AgentTaskTraceContext,\n opts: {\n model: string;\n system?: string;\n messages?: unknown;\n parentSpanId?: string;\n taskId?: string;\n turnNumber?: number;\n /** Serving host attributes for this generation (issue #307). */\n provider?: string;\n /** Service the harness calls (vendor or gateway name), emitted as\n * `apo.llm.api_provider`. */\n apiProvider?: string;\n route?: string;\n },\n): { spanId: string; startedAt: number } {\n const spanId = trace.createSpan({\n task_id: opts.taskId ?? \"trace\",\n parent_call_id: opts.parentSpanId ?? trace.rootSpanId,\n step_name: \"agent.generate\",\n model: opts.model,\n provider: opts.provider,\n api_provider: opts.apiProvider,\n route: opts.route,\n observation_type: \"GENERATION\",\n input: {\n ...(opts.system !== undefined ? { system: opts.system } : {}),\n ...(opts.messages !== undefined ? { messages: opts.messages } : {}),\n },\n metadata: {\n ...(opts.turnNumber !== undefined ? { turnNumber: opts.turnNumber } : {}),\n ...(opts.taskId ? { taskId: opts.taskId } : {}),\n },\n });\n return { spanId, startedAt: monotonicNowMs() };\n}\n\n// Re-export safeParse for the wrappers (OpenAI arguments is a JSON string)\nexport { safeParse };\n"],"mappings":"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","names":[]}
|
|
@@ -6,7 +6,7 @@ import {
|
|
|
6
6
|
servingFromResponse,
|
|
7
7
|
servingHostFromBaseURL,
|
|
8
8
|
startGeneration
|
|
9
|
-
} from "./chunk-
|
|
9
|
+
} from "./chunk-KWIYQSWK.js";
|
|
10
10
|
|
|
11
11
|
// src/agent-task/integrations/openai.ts
|
|
12
12
|
function createApoOpenAI(client, options) {
|
|
@@ -34,7 +34,8 @@ function createApoOpenAI(client, options) {
|
|
|
34
34
|
parentSpanId,
|
|
35
35
|
taskId,
|
|
36
36
|
turnNumber,
|
|
37
|
-
...serving
|
|
37
|
+
...serving,
|
|
38
|
+
apiProvider: serving.provider
|
|
38
39
|
});
|
|
39
40
|
try {
|
|
40
41
|
let response;
|
|
@@ -104,4 +105,4 @@ function createApoOpenAI(client, options) {
|
|
|
104
105
|
export {
|
|
105
106
|
createApoOpenAI
|
|
106
107
|
};
|
|
107
|
-
//# sourceMappingURL=chunk-
|
|
108
|
+
//# sourceMappingURL=chunk-Z7DVZ34X.js.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../src/agent-task/integrations/openai.ts"],"sourcesContent":["/**\n * OpenAI JS SDK tracing integration for apo.\n *\n * Wraps an `OpenAI` client so `chat.completions.create()` calls are\n * automatically traced. Tool calls and generations are captured without\n * any manual span code in the adapter.\n *\n * Uses a Proxy to intercept the `create` method, then emits GENERATION +\n * TOOL spans through the trace context, where they become the observations\n * that `t.calledTool` reads.\n *\n * @example\n * ```ts\n * import OpenAI from \"openai\";\n * import { createApoOpenAI } from \"@apo-ai/sdk/agent-task\";\n *\n * async sendUserTurn(turn, { trace, parentSpanId }) {\n * const client = createApoOpenAI(\n * new OpenAI({ apiKey, baseURL: \"https://openrouter.ai/api/v1\" }),\n * { trace, parentSpanId, taskId: ctx.task.id, turnNumber }\n * );\n * const response = await client.chat.completions.create({\n * model: \"deepseek/deepseek-v4.1-flash\",\n * messages: [{ role: \"user\", content: String(turn) }],\n * tools: [...],\n * });\n * return { response: response.choices[0].message.content ?? \"\" };\n * }\n * ```\n *\n * @module\n */\n\nimport type { AgentTaskTraceContext } from \"../tracing.ts\";\nimport {\n startGeneration,\n emitGenerationAndTools,\n safeParse,\n servingFromResponse,\n servingHostFromBaseURL,\n} from \"./span-helpers.ts\";\n\nexport interface CreateApoOpenAIOptions {\n /** The tee'd trace context from `sendUserTurn`'s second argument. */\n trace: AgentTaskTraceContext;\n /** The parent span id for this turn (from `sendUserTurn`'s context). */\n parentSpanId?: string;\n /** Optional task id, surfaced in span metadata. */\n taskId?: string;\n /** Optional turn number, surfaced in span metadata. */\n turnNumber?: number;\n}\n\n/**\n * Minimal structural shape of the OpenAI client we need to intercept.\n * We avoid importing `openai` at the type level so this module doesn't\n * take a runtime dependency — the wrapper works with any client that has\n * `chat.completions.create`.\n *\n * The `create` signature uses a broad type so the OpenAI SDK's overloaded\n * `create` method (which has specific param types) is assignable to this.\n */\ninterface OpenAIClientLike {\n chat: {\n completions: {\n // eslint-disable-next-line @typescript-eslint/no-explicit-any\n create: (...args: any[]) => Promise<any>;\n // openai SDK ≥4 exposes the raw-response variant; optional so custom\n // clients without it still work.\n withRawResponse?: {\n // eslint-disable-next-line @typescript-eslint/no-explicit-any\n create: (...args: any[]) => Promise<any>;\n };\n };\n };\n // eslint-disable-next-line @typescript-eslint/no-explicit-any\n [key: string]: any;\n}\n\n/**\n * The response shape we read from `chat.completions.create()`.\n */\ninterface OpenAIResponseLike {\n model?: string;\n /** OpenRouter's routing report, opt-in via X-OpenRouter-Metadata. */\n openrouter_metadata?: {\n endpoints?: {\n available?: Array<{ provider?: string; selected?: boolean }>;\n };\n };\n choices?: Array<{\n message?: {\n content?: string | null;\n tool_calls?: Array<{\n function?: { name?: string; arguments?: string };\n }>;\n };\n }>;\n usage?: {\n prompt_tokens?: number;\n completion_tokens?: number;\n completion_tokens_details?: {\n /** Thinking tokens, reported by o-series/DeepSeek/GLM-style providers. */\n reasoning_tokens?: number;\n };\n };\n}\n\n/**\n * Wrap an OpenAI client so `chat.completions.create()` is traced.\n * Returns a Proxy-wrapped client — call it exactly as you would the\n * original. Non-streaming only; streaming responses pass through\n * untraced.\n *\n * For SDKs that emit OpenTelemetry natively (OpenAI Agents SDK, Vercel AI\n * SDK, Claude Agent SDK), prefer `configureApoTelemetry()` instead — no\n * wrapper needed. This manual wrapper is for the raw `openai` package or\n * custom clients that don't emit OTel on their own.\n */\nexport function createApoOpenAI<T extends OpenAIClientLike>(\n client: T,\n options: CreateApoOpenAIOptions,\n): T {\n const { trace, parentSpanId, taskId, turnNumber } = options;\n const realCreate = client.chat.completions.create.bind(client.chat.completions);\n // The raw-response variant (openai SDK ≥4) is how response headers reach\n // us — LiteLLM reports the serving deployment there. Optional: custom\n // clients without it fall back to the plain call.\n const rawCreate:\n | ((...args: unknown[]) => Promise<{\n headers: { get(name: string): string | null };\n parse(): Promise<unknown>;\n }>)\n | undefined = client.chat.completions.withRawResponse?.create?.bind(\n client.chat.completions.withRawResponse,\n );\n\n // eslint-disable-next-line @typescript-eslint/no-explicit-any\n const tracedCreate = async (...args: any[]): Promise<any> => {\n const params = (args[0] ?? {}) as Record<string, unknown>;\n const model = String(params.model ?? \"unknown\");\n\n // Non-streaming: trace fully. Streaming (stream: true): pass through.\n if (params.stream === true) {\n return realCreate(params);\n }\n\n // OpenRouter's routing metadata is opt-in per request; asking for it on\n // every openrouter call costs nothing and yields the selected provider\n // (plus the fallback audit trail OpenRouter then includes).\n const baseURL = (client as { baseURL?: unknown }).baseURL;\n const requestParams =\n typeof baseURL === \"string\" && baseURL.includes(\"openrouter.ai\")\n ? { ...params, headers: { ...(params.headers as object | undefined), \"X-OpenRouter-Metadata\": \"enabled\" } }\n : params;\n\n // The OpenAI SDK is often pointed at a gateway via baseURL — the host\n // it actually calls is the run's serving host (issue #307).\n const serving = servingHostFromBaseURL(\n (client as { baseURL?: unknown }).baseURL,\n \"openai\",\n );\n const { spanId, startedAt } = startGeneration(trace, {\n model,\n system: typeof params.system === \"string\" ? params.system : undefined,\n messages: params.messages,\n parentSpanId,\n taskId,\n turnNumber,\n ...serving,\n });\n\n try {\n let response: OpenAIResponseLike;\n let headers: { get(name: string): string | null } | null = null;\n if (rawCreate) {\n const raw = await rawCreate(requestParams);\n headers = raw.headers;\n response = (await raw.parse()) as OpenAIResponseLike;\n } else {\n response = (await realCreate(requestParams)) as OpenAIResponseLike;\n }\n const message = response.choices?.[0]?.message;\n const text = message?.content ?? undefined;\n const toolCalls = (message?.tool_calls ?? []).map((tc) => ({\n name: tc.function?.name ?? \"unknown\",\n input: tc.function?.arguments ? safeParse(tc.function.arguments) : undefined,\n }));\n // The gateway's own report of who served the call beats the baseURL\n // guess: OpenRouter metadata / LiteLLM response headers.\n const reported = servingFromResponse(response, headers);\n\n emitGenerationAndTools(trace, spanId, startedAt, {\n text,\n servedModel: response.model,\n servedProvider: reported.provider,\n servedRoute: reported.route,\n promptTokens: response.usage?.prompt_tokens,\n completionTokens: response.usage?.completion_tokens,\n // OpenAI-compatible providers report thinking tokens here (o-series,\n // DeepSeek, GLM). Absent detail = unreported, not zero.\n reasoningTokens:\n response.usage?.completion_tokens_details?.reasoning_tokens,\n toolCalls,\n taskId,\n turnNumber,\n });\n\n return response;\n } catch (error) {\n emitGenerationAndTools(trace, spanId, startedAt, {\n taskId,\n turnNumber,\n error: { message: error instanceof Error ? error.message : String(error) },\n });\n throw error;\n }\n };\n\n // Proxy that intercepts client.chat.completions.create but passes\n // everything else through unchanged.\n return new Proxy(client, {\n get(target, prop, receiver) {\n if (prop === \"chat\") {\n return new Proxy(target.chat, {\n get(chatTarget, chatProp) {\n if (chatProp === \"completions\") {\n return new Proxy(chatTarget.completions, {\n get(completionsTarget, completionsProp) {\n if (completionsProp === \"create\") {\n return tracedCreate;\n }\n return Reflect.get(completionsTarget, completionsProp);\n },\n });\n }\n return Reflect.get(chatTarget, chatProp);\n },\n });\n }\n return Reflect.get(target, prop, receiver);\n },\n }) as T;\n}\n"],"mappings":"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","names":[]}
|
|
1
|
+
{"version":3,"sources":["../src/agent-task/integrations/openai.ts"],"sourcesContent":["/**\n * OpenAI JS SDK tracing integration for apo.\n *\n * Wraps an `OpenAI` client so `chat.completions.create()` calls are\n * automatically traced. Tool calls and generations are captured without\n * any manual span code in the adapter.\n *\n * Uses a Proxy to intercept the `create` method, then emits GENERATION +\n * TOOL spans through the trace context, where they become the observations\n * that `t.calledTool` reads.\n *\n * @example\n * ```ts\n * import OpenAI from \"openai\";\n * import { createApoOpenAI } from \"@apo-ai/sdk/agent-task\";\n *\n * async sendUserTurn(turn, { trace, parentSpanId }) {\n * const client = createApoOpenAI(\n * new OpenAI({ apiKey, baseURL: \"https://openrouter.ai/api/v1\" }),\n * { trace, parentSpanId, taskId: ctx.task.id, turnNumber }\n * );\n * const response = await client.chat.completions.create({\n * model: \"deepseek/deepseek-v4.1-flash\",\n * messages: [{ role: \"user\", content: String(turn) }],\n * tools: [...],\n * });\n * return { response: response.choices[0].message.content ?? \"\" };\n * }\n * ```\n *\n * @module\n */\n\nimport type { AgentTaskTraceContext } from \"../tracing.ts\";\nimport {\n startGeneration,\n emitGenerationAndTools,\n safeParse,\n servingFromResponse,\n servingHostFromBaseURL,\n} from \"./span-helpers.ts\";\n\nexport interface CreateApoOpenAIOptions {\n /** The tee'd trace context from `sendUserTurn`'s second argument. */\n trace: AgentTaskTraceContext;\n /** The parent span id for this turn (from `sendUserTurn`'s context). */\n parentSpanId?: string;\n /** Optional task id, surfaced in span metadata. */\n taskId?: string;\n /** Optional turn number, surfaced in span metadata. */\n turnNumber?: number;\n}\n\n/**\n * Minimal structural shape of the OpenAI client we need to intercept.\n * We avoid importing `openai` at the type level so this module doesn't\n * take a runtime dependency — the wrapper works with any client that has\n * `chat.completions.create`.\n *\n * The `create` signature uses a broad type so the OpenAI SDK's overloaded\n * `create` method (which has specific param types) is assignable to this.\n */\ninterface OpenAIClientLike {\n chat: {\n completions: {\n // eslint-disable-next-line @typescript-eslint/no-explicit-any\n create: (...args: any[]) => Promise<any>;\n // openai SDK ≥4 exposes the raw-response variant; optional so custom\n // clients without it still work.\n withRawResponse?: {\n // eslint-disable-next-line @typescript-eslint/no-explicit-any\n create: (...args: any[]) => Promise<any>;\n };\n };\n };\n // eslint-disable-next-line @typescript-eslint/no-explicit-any\n [key: string]: any;\n}\n\n/**\n * The response shape we read from `chat.completions.create()`.\n */\ninterface OpenAIResponseLike {\n model?: string;\n /** OpenRouter's routing report, opt-in via X-OpenRouter-Metadata. */\n openrouter_metadata?: {\n endpoints?: {\n available?: Array<{ provider?: string; selected?: boolean }>;\n };\n };\n choices?: Array<{\n message?: {\n content?: string | null;\n tool_calls?: Array<{\n function?: { name?: string; arguments?: string };\n }>;\n };\n }>;\n usage?: {\n prompt_tokens?: number;\n completion_tokens?: number;\n completion_tokens_details?: {\n /** Thinking tokens, reported by o-series/DeepSeek/GLM-style providers. */\n reasoning_tokens?: number;\n };\n };\n}\n\n/**\n * Wrap an OpenAI client so `chat.completions.create()` is traced.\n * Returns a Proxy-wrapped client — call it exactly as you would the\n * original. Non-streaming only; streaming responses pass through\n * untraced.\n *\n * For SDKs that emit OpenTelemetry natively (OpenAI Agents SDK, Vercel AI\n * SDK, Claude Agent SDK), prefer `configureApoTelemetry()` instead — no\n * wrapper needed. This manual wrapper is for the raw `openai` package or\n * custom clients that don't emit OTel on their own.\n */\nexport function createApoOpenAI<T extends OpenAIClientLike>(\n client: T,\n options: CreateApoOpenAIOptions,\n): T {\n const { trace, parentSpanId, taskId, turnNumber } = options;\n const realCreate = client.chat.completions.create.bind(client.chat.completions);\n // The raw-response variant (openai SDK ≥4) is how response headers reach\n // us — LiteLLM reports the serving deployment there. Optional: custom\n // clients without it fall back to the plain call.\n const rawCreate:\n | ((...args: unknown[]) => Promise<{\n headers: { get(name: string): string | null };\n parse(): Promise<unknown>;\n }>)\n | undefined = client.chat.completions.withRawResponse?.create?.bind(\n client.chat.completions.withRawResponse,\n );\n\n // eslint-disable-next-line @typescript-eslint/no-explicit-any\n const tracedCreate = async (...args: any[]): Promise<any> => {\n const params = (args[0] ?? {}) as Record<string, unknown>;\n const model = String(params.model ?? \"unknown\");\n\n // Non-streaming: trace fully. Streaming (stream: true): pass through.\n if (params.stream === true) {\n return realCreate(params);\n }\n\n // OpenRouter's routing metadata is opt-in per request; asking for it on\n // every openrouter call costs nothing and yields the selected provider\n // (plus the fallback audit trail OpenRouter then includes).\n const baseURL = (client as { baseURL?: unknown }).baseURL;\n const requestParams =\n typeof baseURL === \"string\" && baseURL.includes(\"openrouter.ai\")\n ? { ...params, headers: { ...(params.headers as object | undefined), \"X-OpenRouter-Metadata\": \"enabled\" } }\n : params;\n\n // The OpenAI SDK is often pointed at a gateway via baseURL — the host\n // it actually calls is the run's serving host (issue #307).\n const serving = servingHostFromBaseURL(\n (client as { baseURL?: unknown }).baseURL,\n \"openai\",\n );\n const { spanId, startedAt } = startGeneration(trace, {\n model,\n system: typeof params.system === \"string\" ? params.system : undefined,\n messages: params.messages,\n parentSpanId,\n taskId,\n turnNumber,\n ...serving,\n apiProvider: serving.provider,\n });\n\n try {\n let response: OpenAIResponseLike;\n let headers: { get(name: string): string | null } | null = null;\n if (rawCreate) {\n const raw = await rawCreate(requestParams);\n headers = raw.headers;\n response = (await raw.parse()) as OpenAIResponseLike;\n } else {\n response = (await realCreate(requestParams)) as OpenAIResponseLike;\n }\n const message = response.choices?.[0]?.message;\n const text = message?.content ?? undefined;\n const toolCalls = (message?.tool_calls ?? []).map((tc) => ({\n name: tc.function?.name ?? \"unknown\",\n input: tc.function?.arguments ? safeParse(tc.function.arguments) : undefined,\n }));\n // The gateway's own report of who served the call beats the baseURL\n // guess: OpenRouter metadata / LiteLLM response headers.\n const reported = servingFromResponse(response, headers);\n\n emitGenerationAndTools(trace, spanId, startedAt, {\n text,\n servedModel: response.model,\n servedProvider: reported.provider,\n servedRoute: reported.route,\n promptTokens: response.usage?.prompt_tokens,\n completionTokens: response.usage?.completion_tokens,\n // OpenAI-compatible providers report thinking tokens here (o-series,\n // DeepSeek, GLM). Absent detail = unreported, not zero.\n reasoningTokens:\n response.usage?.completion_tokens_details?.reasoning_tokens,\n toolCalls,\n taskId,\n turnNumber,\n });\n\n return response;\n } catch (error) {\n emitGenerationAndTools(trace, spanId, startedAt, {\n taskId,\n turnNumber,\n error: { message: error instanceof Error ? error.message : String(error) },\n });\n throw error;\n }\n };\n\n // Proxy that intercepts client.chat.completions.create but passes\n // everything else through unchanged.\n return new Proxy(client, {\n get(target, prop, receiver) {\n if (prop === \"chat\") {\n return new Proxy(target.chat, {\n get(chatTarget, chatProp) {\n if (chatProp === \"completions\") {\n return new Proxy(chatTarget.completions, {\n get(completionsTarget, completionsProp) {\n if (completionsProp === \"create\") {\n return tracedCreate;\n }\n return Reflect.get(completionsTarget, completionsProp);\n },\n });\n }\n return Reflect.get(chatTarget, chatProp);\n },\n });\n }\n return Reflect.get(target, prop, receiver);\n },\n }) as T;\n}\n"],"mappings":"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","names":[]}
|
package/dist/otel/index.js
CHANGED
|
@@ -27,6 +27,14 @@ interface CreateSpanParams {
|
|
|
27
27
|
* actually known (vendor SDK or baseURL) — never guessed from the model
|
|
28
28
|
* id; the backend renders absent as unknown. */
|
|
29
29
|
provider?: string;
|
|
30
|
+
/** The service the harness actually calls (vendor or gateway name
|
|
31
|
+
* resolved from the client's baseURL), emitted as `apo.llm.api_provider`.
|
|
32
|
+
* Distinct from the serving host: traffic through a gateway reports the
|
|
33
|
+
* gateway here and the host only when the response names it. */
|
|
34
|
+
api_provider?: string;
|
|
35
|
+
/** Start-time serving-host evidence (e.g. declared through the AI SDK's
|
|
36
|
+
* telemetry metadata), emitted as `apo.llm.serving_host`. */
|
|
37
|
+
serving_host?: string;
|
|
30
38
|
/** Finer-grained serving route (e.g. a custom gateway host), emitted as
|
|
31
39
|
* `apo.llm.route`. */
|
|
32
40
|
route?: string;
|
|
@@ -66,6 +74,15 @@ interface EndSpanParams {
|
|
|
66
74
|
* response headers), emitted as `gen_ai.provider.name`. Set at end time
|
|
67
75
|
* when the gateway reports it — more truthful than any baseURL guess. */
|
|
68
76
|
provider?: string;
|
|
77
|
+
/** The host that executed inference, learned from the response (gateway
|
|
78
|
+
* metadata/headers), emitted as `apo.llm.serving_host`. Never copied
|
|
79
|
+
* from the API provider: gateways make that implication unsafe. */
|
|
80
|
+
serving_host?: string;
|
|
81
|
+
/** The service the harness calls, learned from a child step span's
|
|
82
|
+
* telemetry metadata (the AI SDK puts base telemetry attributes on
|
|
83
|
+
* doGenerate/doStream children, not the parent wrapper), emitted as
|
|
84
|
+
* `apo.llm.api_provider`. */
|
|
85
|
+
api_provider?: string;
|
|
69
86
|
/** Finer-grained serving route learned from the response (e.g. a LiteLLM
|
|
70
87
|
* deployment id), emitted as `apo.llm.route`. */
|
|
71
88
|
route?: string;
|
|
@@ -153,5 +170,11 @@ type AgentTaskTraceOptions = {
|
|
|
153
170
|
*/
|
|
154
171
|
taskRunId?: string;
|
|
155
172
|
};
|
|
173
|
+
/**
|
|
174
|
+
* The slice `t.semantic` uses: the embeddings call becomes an EMBEDDING
|
|
175
|
+
* observation span (nested under the check's span, like judge spans), and
|
|
176
|
+
* the similarity lands as a NUMERIC score record attached to it.
|
|
177
|
+
*/
|
|
178
|
+
type EmbeddingTracer = Pick<AgentTaskTraceContext, "step" | "score">;
|
|
156
179
|
|
|
157
|
-
export type { AgentTaskTraceContext as A, AgentTaskTraceOptions as a };
|
|
180
|
+
export type { AgentTaskTraceContext as A, EmbeddingTracer as E, AgentTaskTraceOptions as a };
|
package/package.json
CHANGED
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"sources":["../src/agent-task/integrations/span-helpers.ts"],"sourcesContent":["/**\n * Shared span-emission helpers for the tracing integrations.\n *\n * The OpenAI and Anthropic wrappers both need to:\n * 1. Create a GENERATION span before the LLM call\n * 2. End it with text/tokens/latency after the response\n * 3. Emit a TOOL span for each tool call in the response\n *\n * These helpers keep that logic in one place so the wrappers stay thin.\n *\n * @internal\n */\n\nimport type { AgentTaskTraceContext } from \"../tracing.ts\";\n\nfunction monotonicNowMs(): number {\n if (typeof performance !== \"undefined\" && typeof performance.now === \"function\") {\n return performance.now();\n }\n return Date.now();\n}\n\nfunction round3(n: number): number {\n return Math.round(n * 1000) / 1000;\n}\n\nfunction safeParse(raw: unknown): unknown {\n if (typeof raw !== \"string\") return raw;\n try {\n return JSON.parse(raw);\n } catch {\n return raw;\n }\n}\n\n/**\n * Emit a GENERATION span + TOOL spans for a completed LLM call.\n *\n * Both wrappers call this after the SDK returns the response. It:\n * 1. Ends the GENERATION span (created by {@link startGeneration}) with\n * text, token counts, and latency.\n * 2. Emits a TOOL span per tool call, so `t.calledTool(name)` works.\n */\nexport function emitGenerationAndTools(\n trace: AgentTaskTraceContext,\n genSpanId: string,\n genStartedAt: number,\n opts: {\n text?: string;\n promptTokens?: number;\n completionTokens?: number;\n /** Reasoning tokens, when the provider reports the dimension. */\n reasoningTokens?: number;\n /** The model that actually served the call (response `model` field). */\n servedModel?: string;\n /** Serving host learned from the response (gateway metadata/headers);\n * overrides the start-time baseURL guess when present. */\n servedProvider?: string;\n servedRoute?: string;\n toolCalls?: Array<{ name: string; input?: unknown }>;\n taskId?: string;\n turnNumber?: number;\n error?: { message: string };\n },\n): void {\n const latency = round3(monotonicNowMs() - genStartedAt);\n const isError = !!opts.error;\n\n // End the GENERATION span\n trace.endSpan(genSpanId, {\n latency_ms: latency,\n prompt_tokens: opts.promptTokens,\n completion_tokens: opts.completionTokens,\n reasoning_tokens: opts.reasoningTokens,\n served_model: opts.servedModel,\n ...(opts.servedProvider ? { provider: opts.servedProvider } : {}),\n ...(opts.servedRoute ? { route: opts.servedRoute } : {}),\n output: {\n ...(opts.text !== undefined ? { text: opts.text } : {}),\n ...(opts.error ? { error: opts.error.message } : {}),\n },\n ...(isError\n ? { level: \"ERROR\" as const, status_message: opts.error!.message }\n : {}),\n });\n\n // Emit TOOL spans (only on success — errors are captured on the GENERATION span)\n if (!isError && opts.toolCalls) {\n for (const tc of opts.toolCalls) {\n const toolSpanId = trace.createSpan({\n task_id: opts.taskId ?? \"trace\",\n parent_call_id: genSpanId,\n step_name: tc.name,\n observation_type: \"TOOL\",\n ...(tc.input !== undefined\n ? {\n input:\n tc.input && typeof tc.input === \"object\"\n ? (tc.input as Record<string, unknown>)\n : { value: tc.input },\n }\n : {}),\n metadata: { toolName: tc.name },\n });\n trace.endSpan(toolSpanId, {});\n }\n }\n}\n\n/**\n * Resolve the serving host a client actually talks to, from its baseURL.\n *\n * The vendor default (no baseURL) means the SDK's own endpoint. A custom\n * baseURL is the serving host itself — for a known gateway the provider is\n * its name; for anything else the registered domain is the provider and the\n * full host the route, which is exactly the split the backend stores\n * (`gen_ai.provider.name` / `apo.llm.route`, issue #307). Local/private\n * hosts are proxies in front of an unknown backend — report nothing rather\n * than a wrong host.\n */\nexport function servingHostFromBaseURL(\n baseURL: unknown,\n defaultVendor?: string,\n): { provider?: string; route?: string } {\n if (typeof baseURL !== \"string\" || baseURL.trim() === \"\") {\n return defaultVendor ? { provider: defaultVendor } : {};\n }\n let host: string;\n try {\n host = new URL(baseURL).hostname;\n } catch {\n return defaultVendor ? { provider: defaultVendor } : {};\n }\n if (\n host === \"localhost\" ||\n /^(127\\.|0\\.0.0\\.0|10\\.|192\\.168\\.|172\\.(1[6-9]|2\\d|3[01])\\.)/.test(host)\n ) {\n // A private host is a gateway in front of an unknown backend. An\n // operator can name it once via APO_SERVING_GATEWAY (e.g. \"litellm\")\n // instead of every run reporting \"unknown\".\n const named = process.env.APO_SERVING_GATEWAY?.trim();\n return named ? { provider: named } : {};\n }\n const known: Record<string, string> = {\n \"api.openai.com\": \"openai\",\n \"api.anthropic.com\": \"anthropic\",\n \"openrouter.ai\": \"openrouter\",\n \"api.fireworks.ai\": \"fireworks\",\n \"api.groq.com\": \"groq\",\n \"api.cerebras.ai\": \"cerebras\",\n \"api.deepseek.com\": \"deepseek\",\n \"api.mistral.ai\": \"mistral\",\n \"api.x.ai\": \"xai\",\n \"api.together.xyz\": \"together\",\n \"generativelanguage.googleapis.com\": \"google\",\n };\n const provider = known[host];\n if (provider) return { provider };\n const labels = host.split(\".\");\n if (labels.length < 2) return {};\n return { provider: labels.slice(-2).join(\".\"), route: host };\n}\n\n/**\n * Derive the serving host a gateway reports about itself, from the response.\n *\n * Gateways in front of many backends know exactly which one served a call;\n * the baseURL only ever names the gateway. Priority:\n * 1. OpenRouter's opt-in `openrouter_metadata.endpoints.available[selected]`\n * (send `X-OpenRouter-Metadata: enabled` to receive it).\n * 2. LiteLLM's response headers: `x-litellm-model-id` (the deployment)\n * and `x-litellm-model-api-base` (the upstream actually called).\n * Returns {} when the gateway reports nothing — unknown stays unknown.\n */\nexport function servingFromResponse(\n response: {\n openrouter_metadata?: {\n endpoints?: {\n available?: Array<{ provider?: unknown; selected?: unknown }>;\n };\n };\n },\n headers?: { get(name: string): string | null } | null,\n): { provider?: string; route?: string } {\n const endpoints = response?.openrouter_metadata?.endpoints?.available;\n if (Array.isArray(endpoints)) {\n const selected = endpoints.find(\n (e) => e && typeof e === \"object\" && e.selected === true,\n );\n const provider =\n selected && typeof (selected as { provider?: unknown }).provider === \"string\"\n ? (selected as { provider: string }).provider\n : undefined;\n if (provider) {\n return { provider: \"openrouter\", route: `openrouter:${provider}` };\n }\n }\n const litellmId = headers?.get?.(\"x-litellm-model-id\");\n if (typeof litellmId === \"string\" && litellmId) {\n const upstream = headers?.get?.(\"x-litellm-model-api-base\");\n const host =\n typeof upstream === \"string\" && upstream\n ? safeUrlHost(upstream)\n : undefined;\n return {\n provider: \"litellm\",\n route: host ? `litellm:${litellmId}@${host}` : `litellm:${litellmId}`,\n };\n }\n return {};\n}\n\nfunction safeUrlHost(url: string): string | undefined {\n try {\n return new URL(url).hostname;\n } catch {\n return undefined;\n }\n}\n\n/**\n * Create a GENERATION span before the LLM call.\n * Returns `{ spanId, startedAt }` to pass to {@link emitGenerationAndTools}.\n */\nexport function startGeneration(\n trace: AgentTaskTraceContext,\n opts: {\n model: string;\n system?: string;\n messages?: unknown;\n parentSpanId?: string;\n taskId?: string;\n turnNumber?: number;\n /** Serving host attributes for this generation (issue #307). */\n provider?: string;\n route?: string;\n },\n): { spanId: string; startedAt: number } {\n const spanId = trace.createSpan({\n task_id: opts.taskId ?? \"trace\",\n parent_call_id: opts.parentSpanId ?? trace.rootSpanId,\n step_name: \"agent.generate\",\n model: opts.model,\n provider: opts.provider,\n route: opts.route,\n observation_type: \"GENERATION\",\n input: {\n ...(opts.system !== undefined ? { system: opts.system } : {}),\n ...(opts.messages !== undefined ? { messages: opts.messages } : {}),\n },\n metadata: {\n ...(opts.turnNumber !== undefined ? { turnNumber: opts.turnNumber } : {}),\n ...(opts.taskId ? { taskId: opts.taskId } : {}),\n },\n });\n return { spanId, startedAt: monotonicNowMs() };\n}\n\n// Re-export safeParse for the wrappers (OpenAI arguments is a JSON string)\nexport { safeParse };\n"],"mappings":"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","names":[]}
|