@apo-ai/sdk 0.13.0 → 0.15.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.
@@ -4,7 +4,7 @@ import {
4
4
  emitGenerationAndTools,
5
5
  servingHostFromBaseURL,
6
6
  startGeneration
7
- } from "./chunk-JTWQD27M.js";
7
+ } from "./chunk-JN7RHBTS.js";
8
8
 
9
9
  // src/agent-task/integrations/anthropic.ts
10
10
  function createApoAnthropic(client, options) {
@@ -78,4 +78,4 @@ function createApoAnthropic(client, options) {
78
78
  export {
79
79
  createApoAnthropic
80
80
  };
81
- //# sourceMappingURL=chunk-X73JGMK3.js.map
81
+ //# sourceMappingURL=chunk-4BQSCD6T.js.map
@@ -28,6 +28,8 @@ function emitGenerationAndTools(trace, genSpanId, genStartedAt, opts) {
28
28
  completion_tokens: opts.completionTokens,
29
29
  reasoning_tokens: opts.reasoningTokens,
30
30
  served_model: opts.servedModel,
31
+ ...opts.servedProvider ? { provider: opts.servedProvider } : {},
32
+ ...opts.servedRoute ? { route: opts.servedRoute } : {},
31
33
  output: {
32
34
  ...opts.text !== void 0 ? { text: opts.text } : {},
33
35
  ...opts.error ? { error: opts.error.message } : {}
@@ -60,8 +62,9 @@ function servingHostFromBaseURL(baseURL, defaultVendor) {
60
62
  } catch {
61
63
  return defaultVendor ? { provider: defaultVendor } : {};
62
64
  }
63
- if (host === "localhost" || /^(127\.|0\.0\.0\.0|10\.|192\.168\.|172\.(1[6-9]|2\d|3[01])\.)/.test(host)) {
64
- return {};
65
+ if (host === "localhost" || /^(127\.|0\.0.0\.0|10\.|192\.168\.|172\.(1[6-9]|2\d|3[01])\.)/.test(host)) {
66
+ const named = process.env.APO_SERVING_GATEWAY?.trim();
67
+ return named ? { provider: named } : {};
65
68
  }
66
69
  const known = {
67
70
  "api.openai.com": "openai",
@@ -82,6 +85,35 @@ function servingHostFromBaseURL(baseURL, defaultVendor) {
82
85
  if (labels.length < 2) return {};
83
86
  return { provider: labels.slice(-2).join("."), route: host };
84
87
  }
88
+ function servingFromResponse(response, headers) {
89
+ const endpoints = response?.openrouter_metadata?.endpoints?.available;
90
+ if (Array.isArray(endpoints)) {
91
+ const selected = endpoints.find(
92
+ (e) => e && typeof e === "object" && e.selected === true
93
+ );
94
+ const provider = selected && typeof selected.provider === "string" ? selected.provider : void 0;
95
+ if (provider) {
96
+ return { provider: "openrouter", route: `openrouter:${provider}` };
97
+ }
98
+ }
99
+ const litellmId = headers?.get?.("x-litellm-model-id");
100
+ if (typeof litellmId === "string" && litellmId) {
101
+ const upstream = headers?.get?.("x-litellm-model-api-base");
102
+ const host = typeof upstream === "string" && upstream ? safeUrlHost(upstream) : void 0;
103
+ return {
104
+ provider: "litellm",
105
+ route: host ? `litellm:${litellmId}@${host}` : `litellm:${litellmId}`
106
+ };
107
+ }
108
+ return {};
109
+ }
110
+ function safeUrlHost(url) {
111
+ try {
112
+ return new URL(url).hostname;
113
+ } catch {
114
+ return void 0;
115
+ }
116
+ }
85
117
  function startGeneration(trace, opts) {
86
118
  const spanId = trace.createSpan({
87
119
  task_id: opts.taskId ?? "trace",
@@ -107,6 +139,7 @@ export {
107
139
  safeParse,
108
140
  emitGenerationAndTools,
109
141
  servingHostFromBaseURL,
142
+ servingFromResponse,
110
143
  startGeneration
111
144
  };
112
- //# sourceMappingURL=chunk-JTWQD27M.js.map
145
+ //# sourceMappingURL=chunk-JN7RHBTS.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 ...(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":[]}
@@ -3,20 +3,26 @@
3
3
  import {
4
4
  emitGenerationAndTools,
5
5
  safeParse,
6
+ servingFromResponse,
6
7
  servingHostFromBaseURL,
7
8
  startGeneration
8
- } from "./chunk-JTWQD27M.js";
9
+ } from "./chunk-JN7RHBTS.js";
9
10
 
10
11
  // src/agent-task/integrations/openai.ts
11
12
  function createApoOpenAI(client, options) {
12
13
  const { trace, parentSpanId, taskId, turnNumber } = options;
13
14
  const realCreate = client.chat.completions.create.bind(client.chat.completions);
15
+ const rawCreate = client.chat.completions.withRawResponse?.create?.bind(
16
+ client.chat.completions.withRawResponse
17
+ );
14
18
  const tracedCreate = async (...args) => {
15
19
  const params = args[0] ?? {};
16
20
  const model = String(params.model ?? "unknown");
17
21
  if (params.stream === true) {
18
22
  return realCreate(params);
19
23
  }
24
+ const baseURL = client.baseURL;
25
+ const requestParams = typeof baseURL === "string" && baseURL.includes("openrouter.ai") ? { ...params, headers: { ...params.headers, "X-OpenRouter-Metadata": "enabled" } } : params;
20
26
  const serving = servingHostFromBaseURL(
21
27
  client.baseURL,
22
28
  "openai"
@@ -31,16 +37,27 @@ function createApoOpenAI(client, options) {
31
37
  ...serving
32
38
  });
33
39
  try {
34
- const response = await realCreate(params);
40
+ let response;
41
+ let headers = null;
42
+ if (rawCreate) {
43
+ const raw = await rawCreate(requestParams);
44
+ headers = raw.headers;
45
+ response = await raw.parse();
46
+ } else {
47
+ response = await realCreate(requestParams);
48
+ }
35
49
  const message = response.choices?.[0]?.message;
36
50
  const text = message?.content ?? void 0;
37
51
  const toolCalls = (message?.tool_calls ?? []).map((tc) => ({
38
52
  name: tc.function?.name ?? "unknown",
39
53
  input: tc.function?.arguments ? safeParse(tc.function.arguments) : void 0
40
54
  }));
55
+ const reported = servingFromResponse(response, headers);
41
56
  emitGenerationAndTools(trace, spanId, startedAt, {
42
57
  text,
43
58
  servedModel: response.model,
59
+ servedProvider: reported.provider,
60
+ servedRoute: reported.route,
44
61
  promptTokens: response.usage?.prompt_tokens,
45
62
  completionTokens: response.usage?.completion_tokens,
46
63
  // OpenAI-compatible providers report thinking tokens here (o-series,
@@ -87,4 +104,4 @@ function createApoOpenAI(client, options) {
87
104
  export {
88
105
  createApoOpenAI
89
106
  };
90
- //# sourceMappingURL=chunk-A3UYQ7M4.js.map
107
+ //# sourceMappingURL=chunk-NSMKSPBU.js.map
@@ -0,0 +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":[]}
@@ -49,11 +49,26 @@ interface EndSpanParams {
49
49
  /** Reasoning/thinking tokens, when the provider reports them as a distinct
50
50
  * dimension. Omit when unreported — unknown must stay unknown, not zero. */
51
51
  reasoning_tokens?: number;
52
+ /** Prompt tokens read from the provider's prompt cache. Setting either cache
53
+ * field declares `prompt_tokens` the whole prompt, cached tokens included
54
+ * (OTel GenAI semconv), whatever the model's own API reports; the span then
55
+ * carries `apo.usage.input_includes_cache` so the backend prices it that way. */
56
+ cache_read_tokens?: number;
57
+ /** Prompt tokens written to the provider's prompt cache; part of
58
+ * `prompt_tokens` like `cache_read_tokens`. */
59
+ cache_creation_tokens?: number;
52
60
  /** The model the provider reports as having served the call (the response's
53
61
  * `model` field), emitted as `gen_ai.response.model`. A gateway fallback
54
62
  * (LiteLLM router, OpenRouter provider fallback) can serve a different
55
63
  * model than requested — this is the only field that tells them apart. */
56
64
  served_model?: string;
65
+ /** Serving host learned from the response itself (gateway metadata or
66
+ * response headers), emitted as `gen_ai.provider.name`. Set at end time
67
+ * when the gateway reports it — more truthful than any baseURL guess. */
68
+ provider?: string;
69
+ /** Finer-grained serving route learned from the response (e.g. a LiteLLM
70
+ * deployment id), emitted as `apo.llm.route`. */
71
+ route?: string;
57
72
  status_message?: string;
58
73
  level?: "DEBUG" | "DEFAULT" | "WARNING" | "ERROR";
59
74
  metadata?: Record<string, unknown>;
@@ -72,10 +87,17 @@ interface TraceStepOptions extends Omit<CreateSpanParams, "project" | "task_id"
72
87
  * spans whose token counts are only known after the work (e.g. judge
73
88
  * spans, whose usage rides the judged result). Applied when the span ends;
74
89
  * the extracted counts become `gen_ai.usage.*` span attributes.
90
+ * `served_model` (the model the provider reports as having served the
91
+ * call) becomes `gen_ai.response.model` — a gateway fallback diverges
92
+ * from the requested model, and this is the only field that tells them
93
+ * apart.
75
94
  */
76
95
  usage?: (result: unknown) => {
77
96
  prompt_tokens?: number;
78
97
  completion_tokens?: number;
98
+ cache_read_tokens?: number;
99
+ cache_creation_tokens?: number;
100
+ served_model?: string;
79
101
  } | undefined;
80
102
  }
81
103
  interface TraceEventOptions extends Omit<CreateSpanParams, "project" | "task_id" | "run_id"> {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@apo-ai/sdk",
3
- "version": "0.13.0",
3
+ "version": "0.15.0",
4
4
  "description": "TypeScript SDK for executable agent capability specifications and harness engineering with Apo",
5
5
  "keywords": [
6
6
  "ai-agents",
@@ -79,6 +79,7 @@
79
79
  "vitest": "^4.1.9"
80
80
  },
81
81
  "dependencies": {
82
+ "@ai-sdk/anthropic": "^4.0.74",
82
83
  "@ai-sdk/mcp": "^2.0.62",
83
84
  "@ai-sdk/openai-compatible": "^3.0.48",
84
85
  "@opentelemetry/api": "^1.9.1",
@@ -1 +0,0 @@
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 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 };\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 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\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 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 const response = (await realCreate(params)) as OpenAIResponseLike;\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\n emitGenerationAndTools(trace, spanId, startedAt, {\n text,\n servedModel: response.model,\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":";;;;;;;;;;AA0GO,SAAS,gBACd,QACA,SACG;AACH,QAAM,EAAE,OAAO,cAAc,QAAQ,WAAW,IAAI;AACpD,QAAM,aAAa,OAAO,KAAK,YAAY,OAAO,KAAK,OAAO,KAAK,WAAW;AAG9E,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,IACL,CAAC;AAED,QAAI;AACF,YAAM,WAAY,MAAM,WAAW,MAAM;AACzC,YAAM,UAAU,SAAS,UAAU,CAAC,GAAG;AACvC,YAAM,OAAO,SAAS,WAAW;AACjC,YAAM,aAAa,SAAS,cAAc,CAAC,GAAG,IAAI,CAAC,QAAQ;AAAA,QACzD,MAAM,GAAG,UAAU,QAAQ;AAAA,QAC3B,OAAO,GAAG,UAAU,YAAY,UAAU,GAAG,SAAS,SAAS,IAAI;AAAA,MACrE,EAAE;AAEF,6BAAuB,OAAO,QAAQ,WAAW;AAAA,QAC/C;AAAA,QACA,aAAa,SAAS;AAAA,QACtB,cAAc,SAAS,OAAO;AAAA,QAC9B,kBAAkB,SAAS,OAAO;AAAA;AAAA;AAAA,QAGlC,iBACE,SAAS,OAAO,2BAA2B;AAAA,QAC7C;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,QAAQ;AACnB,eAAO,IAAI,MAAM,OAAO,MAAM;AAAA,UAC5B,IAAI,YAAY,UAAU;AACxB,gBAAI,aAAa,eAAe;AAC9B,qBAAO,IAAI,MAAM,WAAW,aAAa;AAAA,gBACvC,IAAI,mBAAmB,iBAAiB;AACtC,sBAAI,oBAAoB,UAAU;AAChC,2BAAO;AAAA,kBACT;AACA,yBAAO,QAAQ,IAAI,mBAAmB,eAAe;AAAA,gBACvD;AAAA,cACF,CAAC;AAAA,YACH;AACA,mBAAO,QAAQ,IAAI,YAAY,QAAQ;AAAA,UACzC;AAAA,QACF,CAAC;AAAA,MACH;AACA,aAAO,QAAQ,IAAI,QAAQ,MAAM,QAAQ;AAAA,IAC3C;AAAA,EACF,CAAC;AACH;","names":[]}
@@ -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 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 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 return {};\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 * 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":[]}