@heystack/otel 0.12.0 → 0.14.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.
@@ -8,12 +8,112 @@ export function detectLLMProvider(host) {
8
8
  return "openai";
9
9
  if (h === "api.anthropic.com")
10
10
  return "anthropic";
11
+ if (h === "api.mistral.ai")
12
+ return "mistral";
11
13
  if (h === "generativelanguage.googleapis.com")
12
14
  return "google";
13
15
  if (h === "gateway.ai.cloudflare.com" || h.endsWith(".gateway.ai.cloudflare.com"))
14
16
  return "cloudflare";
15
17
  return null;
16
18
  }
19
+ /** Map a gateway path token to a provider. `gemini` → google, `workers-ai` → cloudflare. */
20
+ export function providerFromToken(token) {
21
+ switch (token.toLowerCase()) {
22
+ case "openai":
23
+ return "openai";
24
+ case "anthropic":
25
+ return "anthropic";
26
+ case "google":
27
+ case "gemini":
28
+ return "google";
29
+ case "mistral":
30
+ return "mistral";
31
+ case "cloudflare":
32
+ case "workers-ai":
33
+ return "cloudflare";
34
+ default:
35
+ return null;
36
+ }
37
+ }
38
+ function hostMatchesSuffix(host, suffix) {
39
+ const s = suffix.toLowerCase().replace(/^\.+/, "");
40
+ if (!s)
41
+ return false;
42
+ return host === s || host.endsWith("." + s);
43
+ }
44
+ /** Split a path into non-empty segments. */
45
+ function segments(path) {
46
+ return path.split("/").filter((s) => s.length > 0);
47
+ }
48
+ /**
49
+ * Resolve the LLM provider for an outbound URL: direct host table first, then
50
+ * each configured gateway pattern. Returns null when neither matches. Pure and
51
+ * exception-free (a malformed gateway entry is skipped, never thrown).
52
+ */
53
+ export function resolveLLMProvider(url, gateways) {
54
+ const host = url.hostname.toLowerCase();
55
+ const direct = detectLLMProvider(host);
56
+ if (direct)
57
+ return { provider: direct };
58
+ if (!gateways || gateways.length === 0)
59
+ return null;
60
+ const pathSegs = segments(url.pathname);
61
+ for (const gw of gateways) {
62
+ try {
63
+ if (!gw || typeof gw.hostSuffix !== "string" || typeof gw.pathPattern !== "string")
64
+ continue;
65
+ if (!hostMatchesSuffix(host, gw.hostSuffix))
66
+ continue;
67
+ const patternSegs = segments(gw.pathPattern);
68
+ const providerIdx = patternSegs.findIndex((s) => s.toLowerCase() === ":provider");
69
+ if (providerIdx === -1 || pathSegs.length < patternSegs.length)
70
+ continue;
71
+ let ok = true;
72
+ for (let i = 0; i < patternSegs.length; i++) {
73
+ const p = patternSegs[i];
74
+ if (p.startsWith(":"))
75
+ continue; // any single segment
76
+ if (p !== decodeURIComponentSafe(pathSegs[i])) {
77
+ ok = false;
78
+ break;
79
+ }
80
+ }
81
+ if (!ok)
82
+ continue;
83
+ const provider = providerFromToken(decodeURIComponentSafe(pathSegs[providerIdx]));
84
+ if (provider)
85
+ return { provider, gateway: host };
86
+ }
87
+ catch {
88
+ // malformed entry — skip
89
+ }
90
+ }
91
+ return null;
92
+ }
93
+ function decodeURIComponentSafe(s) {
94
+ try {
95
+ return decodeURIComponent(s);
96
+ }
97
+ catch {
98
+ return s;
99
+ }
100
+ }
101
+ /**
102
+ * Best-effort `gen_ai.operation.name` from the request path. Chat-style
103
+ * endpoints (OpenAI/Mistral `chat/completions`, Anthropic `messages`, Google
104
+ * `*generateContent`) → `chat`; `embeddings` → `embeddings`; bare/fim
105
+ * `completions` → `text_completion`; anything else defaults to `chat`.
106
+ */
107
+ export function llmOperationName(pathname) {
108
+ const p = pathname.toLowerCase();
109
+ if (p.includes("/embeddings"))
110
+ return "embeddings";
111
+ if (p.includes("/chat/completions") || p.includes("/messages") || p.includes("generatecontent"))
112
+ return "chat";
113
+ if (p.endsWith("/completions") || p.includes("/completions"))
114
+ return "text_completion";
115
+ return "chat";
116
+ }
17
117
  /** Best-effort gen_ai.request.* from a parsed JSON request body. Never throws. */
18
118
  export function llmRequestAttrs(provider, body) {
19
119
  const out = { "gen_ai.system": provider };
@@ -37,30 +137,29 @@ export function llmResponseAttrs(provider, body) {
37
137
  if (!body || typeof body !== "object" || Array.isArray(body))
38
138
  return out;
39
139
  const b = body;
140
+ // Response model / id. OpenAI, Mistral (OpenAI-compatible) and Anthropic use
141
+ // `model` / `id`; Google uses `modelVersion` / `responseId`.
40
142
  if (typeof b["model"] === "string")
41
143
  out["gen_ai.response.model"] = b["model"];
144
+ else if (typeof b["modelVersion"] === "string")
145
+ out["gen_ai.response.model"] = b["modelVersion"];
42
146
  if (typeof b["id"] === "string")
43
147
  out["gen_ai.response.id"] = b["id"];
44
- // Usage tokens — OpenAI: usage.prompt_tokens / completion_tokens
45
- // Anthropic: usage.input_tokens / output_tokens
46
- if (b["usage"] && typeof b["usage"] === "object" && !Array.isArray(b["usage"])) {
47
- const usage = b["usage"];
48
- // OpenAI shape
49
- if (typeof usage["prompt_tokens"] === "number") {
50
- out["gen_ai.usage.input_tokens"] = usage["prompt_tokens"];
51
- }
52
- if (typeof usage["completion_tokens"] === "number") {
53
- out["gen_ai.usage.output_tokens"] = usage["completion_tokens"];
54
- }
55
- // Anthropic shape (overrides OpenAI if both somehow present)
56
- if (typeof usage["input_tokens"] === "number") {
57
- out["gen_ai.usage.input_tokens"] = usage["input_tokens"];
58
- }
59
- if (typeof usage["output_tokens"] === "number") {
60
- out["gen_ai.usage.output_tokens"] = usage["output_tokens"];
61
- }
148
+ else if (typeof b["responseId"] === "string")
149
+ out["gen_ai.response.id"] = b["responseId"];
150
+ // Usage tokens — OpenAI/Mistral: usage.prompt_tokens / completion_tokens
151
+ // Anthropic: usage.input_tokens / output_tokens
152
+ // Google: usageMetadata.promptTokenCount / candidatesTokenCount
153
+ Object.assign(out, usageAttrs(b["usage"]));
154
+ if (b["usageMetadata"] && typeof b["usageMetadata"] === "object" && !Array.isArray(b["usageMetadata"])) {
155
+ const um = b["usageMetadata"];
156
+ if (typeof um["promptTokenCount"] === "number")
157
+ out["gen_ai.usage.input_tokens"] = um["promptTokenCount"];
158
+ if (typeof um["candidatesTokenCount"] === "number")
159
+ out["gen_ai.usage.output_tokens"] = um["candidatesTokenCount"];
62
160
  }
63
- // Finish reason — OpenAI: choices[0].finish_reason; Anthropic: stop_reason
161
+ // Finish reason — OpenAI/Mistral: choices[0].finish_reason; Anthropic:
162
+ // stop_reason; Google: candidates[0].finishReason
64
163
  if (typeof b["stop_reason"] === "string") {
65
164
  out["gen_ai.response.finish_reason"] = b["stop_reason"];
66
165
  }
@@ -70,9 +169,183 @@ export function llmResponseAttrs(provider, body) {
70
169
  out["gen_ai.response.finish_reason"] = choice["finish_reason"];
71
170
  }
72
171
  }
172
+ else if (Array.isArray(b["candidates"]) && b["candidates"].length > 0) {
173
+ const cand = b["candidates"][0];
174
+ if (cand && typeof cand === "object" && typeof cand["finishReason"] === "string") {
175
+ out["gen_ai.response.finish_reason"] = cand["finishReason"];
176
+ }
177
+ }
73
178
  void provider; // provider available for future provider-specific logic
74
179
  return out;
75
180
  }
181
+ /** `gen_ai.usage.*` from an OpenAI-style or Anthropic-style `usage` object. */
182
+ function usageAttrs(usage) {
183
+ const out = {};
184
+ if (!usage || typeof usage !== "object" || Array.isArray(usage))
185
+ return out;
186
+ const u = usage;
187
+ // OpenAI / Mistral shape
188
+ if (typeof u["prompt_tokens"] === "number")
189
+ out["gen_ai.usage.input_tokens"] = u["prompt_tokens"];
190
+ if (typeof u["completion_tokens"] === "number")
191
+ out["gen_ai.usage.output_tokens"] = u["completion_tokens"];
192
+ // Anthropic shape (overrides OpenAI if both somehow present)
193
+ if (typeof u["input_tokens"] === "number")
194
+ out["gen_ai.usage.input_tokens"] = u["input_tokens"];
195
+ if (typeof u["output_tokens"] === "number")
196
+ out["gen_ai.usage.output_tokens"] = u["output_tokens"];
197
+ return out;
198
+ }
199
+ // ---------------------------------------------------------------------------
200
+ // Streaming (SSE) usage scanning — metadata only, never content
201
+ //
202
+ // Every provider streams `text/event-stream`; token usage arrives in one or two
203
+ // chunks: OpenAI (with `stream_options.include_usage`) and Mistral put a `usage`
204
+ // object in the final data chunk; Anthropic sends `message_start` (model, id,
205
+ // input_tokens) and `message_delta` (output_tokens, stop_reason); Google sends
206
+ // `usageMetadata` on the final chunk. The scanner parses each `data:` line's
207
+ // JSON just long enough to lift those fields and discards the chunk. Content
208
+ // deltas are never retained or copied into attributes.
209
+ // ---------------------------------------------------------------------------
210
+ /** Lines longer than this are skipped unparsed (a bound on per-chunk JSON.parse). */
211
+ export const SSE_MAX_LINE_BYTES = 64 * 1024;
212
+ /**
213
+ * Metadata attrs from ONE parsed stream chunk. Shares the response mapper and
214
+ * adds the Anthropic envelope (`message.{id,model,usage}` on `message_start`,
215
+ * `delta.stop_reason` on `message_delta`). Never throws.
216
+ */
217
+ export function llmStreamChunkAttrs(provider, chunk) {
218
+ const out = llmResponseAttrs(provider, chunk);
219
+ if (!chunk || typeof chunk !== "object" || Array.isArray(chunk))
220
+ return out;
221
+ const c = chunk;
222
+ const msg = c["message"];
223
+ if (msg && typeof msg === "object" && !Array.isArray(msg)) {
224
+ const m = msg;
225
+ if (typeof m["id"] === "string")
226
+ out["gen_ai.response.id"] = m["id"];
227
+ if (typeof m["model"] === "string")
228
+ out["gen_ai.response.model"] = m["model"];
229
+ Object.assign(out, usageAttrs(m["usage"]));
230
+ }
231
+ const delta = c["delta"];
232
+ if (delta && typeof delta === "object" && !Array.isArray(delta)) {
233
+ const d = delta;
234
+ if (typeof d["stop_reason"] === "string")
235
+ out["gen_ai.response.finish_reason"] = d["stop_reason"];
236
+ }
237
+ return out;
238
+ }
239
+ /**
240
+ * Incremental SSE scanner: buffers at most one partial line, parses complete
241
+ * `data:` lines for usage/model/id/finish_reason and drops everything else
242
+ * immediately. Lines over `SSE_MAX_LINE_BYTES` are skipped without parsing.
243
+ */
244
+ export function createSseUsageScanner(provider) {
245
+ const acc = {};
246
+ let buf = "";
247
+ // When a line has already exceeded the cap, keep discarding until its newline.
248
+ let skipping = false;
249
+ const handleLine = (raw) => {
250
+ const line = raw.endsWith("\r") ? raw.slice(0, -1) : raw;
251
+ if (!line.startsWith("data:"))
252
+ return; // event:/id:/retry:/comments
253
+ const payload = line.slice(5).trim();
254
+ if (!payload || payload === "[DONE]")
255
+ return;
256
+ if (payload.charCodeAt(0) !== 123 /* { */)
257
+ return;
258
+ try {
259
+ const chunk = JSON.parse(payload);
260
+ const attrs = llmStreamChunkAttrs(provider, chunk);
261
+ for (const k of Object.keys(attrs))
262
+ acc[k] = attrs[k];
263
+ }
264
+ catch {
265
+ // not JSON — ignore this line
266
+ }
267
+ };
268
+ return {
269
+ feed(text) {
270
+ try {
271
+ let start = 0;
272
+ for (;;) {
273
+ const nl = text.indexOf("\n", start);
274
+ if (nl === -1) {
275
+ const rest = text.slice(start);
276
+ if (skipping)
277
+ break;
278
+ if (buf.length + rest.length > SSE_MAX_LINE_BYTES) {
279
+ buf = "";
280
+ skipping = true;
281
+ break;
282
+ }
283
+ buf += rest;
284
+ break;
285
+ }
286
+ const piece = text.slice(start, nl);
287
+ start = nl + 1;
288
+ if (skipping) {
289
+ skipping = false;
290
+ buf = "";
291
+ continue;
292
+ }
293
+ if (buf.length + piece.length > SSE_MAX_LINE_BYTES) {
294
+ buf = "";
295
+ continue; // oversize line — skip it whole
296
+ }
297
+ const line = buf + piece;
298
+ buf = "";
299
+ handleLine(line);
300
+ }
301
+ }
302
+ catch {
303
+ // never throw into the caller's stream
304
+ }
305
+ },
306
+ result() {
307
+ if (buf && !skipping) {
308
+ const line = buf;
309
+ buf = "";
310
+ handleLine(line);
311
+ }
312
+ return { ...acc };
313
+ },
314
+ };
315
+ }
316
+ /**
317
+ * Drain a (teed) SSE body through the scanner and resolve with the accumulated
318
+ * metadata attrs when the stream closes. Resolves — never rejects — on a stream
319
+ * error, returning whatever was scanned so far. Reads chunk-by-chunk with a
320
+ * streaming `TextDecoder`, so the body is never buffered.
321
+ */
322
+ export async function scanSseStreamForUsage(stream, provider) {
323
+ const scanner = createSseUsageScanner(provider);
324
+ const decoder = new TextDecoder();
325
+ const reader = stream.getReader();
326
+ try {
327
+ for (;;) {
328
+ const { done, value } = await reader.read();
329
+ if (done)
330
+ break;
331
+ if (value)
332
+ scanner.feed(decoder.decode(value, { stream: true }));
333
+ }
334
+ scanner.feed(decoder.decode());
335
+ }
336
+ catch {
337
+ // upstream error/cancel — keep what we have
338
+ }
339
+ finally {
340
+ try {
341
+ reader.releaseLock();
342
+ }
343
+ catch {
344
+ // ignore
345
+ }
346
+ }
347
+ return scanner.result();
348
+ }
76
349
  /** Content attrs, ONLY when captureContent is on. Size-capped per value, redactor applied. */
77
350
  export function llmContentAttrs(provider, reqBody, resBody, opts) {
78
351
  const out = {};
package/dist/node.js CHANGED
@@ -3,7 +3,7 @@ import { OTLPTraceExporter } from "@opentelemetry/exporter-trace-otlp-http";
3
3
  import { getNodeAutoInstrumentations } from "@opentelemetry/auto-instrumentations-node";
4
4
  import { diag, DiagConsoleLogger, DiagLogLevel } from "@opentelemetry/api";
5
5
  import { ExportResultCode } from "@opentelemetry/core";
6
- import { Resource } from "@opentelemetry/resources";
6
+ import { resourceFromAttributes } from "@opentelemetry/resources";
7
7
  import { buildExporterConfig, releaseResourceAttributes, } from "./core.js";
8
8
  import { isSelfSpanAttrs, safeHostname } from "./self-span.js";
9
9
  /**
@@ -132,7 +132,7 @@ export function initHeystack(o) {
132
132
  // Release/commit attribution. NodeSDK merges `serviceName` on top of this
133
133
  // resource, so the final resource carries service.name + (when provided)
134
134
  // service.version + service.build. Empty when neither option nor env is set.
135
- resource: new Resource(resolveNodeReleaseAttributes(o)),
135
+ resource: resourceFromAttributes(resolveNodeReleaseAttributes(o)),
136
136
  traceExporter,
137
137
  instrumentations,
138
138
  });
@@ -0,0 +1,49 @@
1
+ import { type Span } from "@opentelemetry/api";
2
+ import { type CloudflareNativeTracing } from "./workers";
3
+ /** Structural equivalent of the Agents SDK event; no `agents` peer required. */
4
+ export interface CloudflareAgentEvent {
5
+ type: string;
6
+ agent?: string;
7
+ name?: string;
8
+ payload?: Record<string, unknown>;
9
+ timestamp?: number;
10
+ [key: string]: unknown;
11
+ }
12
+ export interface CloudflareAgentsObservability {
13
+ emit(event: CloudflareAgentEvent): void;
14
+ }
15
+ export interface CloudflareAgentsObservabilityOptions {
16
+ /** Static fields added to every structured event and active-span event. */
17
+ attributes?: Record<string, string | number | boolean>;
18
+ /** Allow-list event types. By default every Agents SDK event is retained. */
19
+ include?: readonly string[];
20
+ /** Additional payload keys to redact. Common secret keys are always redacted. */
21
+ redactKeys?: readonly string[];
22
+ /** Override the structured-log sink (useful for tests/custom routing). */
23
+ logger?: (level: "info" | "error", value: Record<string, unknown>) => void;
24
+ }
25
+ /**
26
+ * Agents SDK `Observability` implementation. It emits one structured log for
27
+ * every lifecycle event and mirrors safe scalar metadata onto the active span.
28
+ * Native Workers log export then preserves trace correlation without an extra
29
+ * network request in the Agent/Durable Object request path.
30
+ */
31
+ export declare function createCloudflareAgentsObservability(options?: CloudflareAgentsObservabilityOptions): CloudflareAgentsObservability;
32
+ export interface CloudflareDiagnosticsMessage {
33
+ timestamp?: number;
34
+ channel?: string;
35
+ message?: CloudflareAgentEvent | unknown;
36
+ }
37
+ export interface CloudflareTailEvent {
38
+ diagnosticsChannelEvents?: CloudflareDiagnosticsMessage[];
39
+ }
40
+ /**
41
+ * Tail Worker handler for production Agents diagnostics channels. Attach the
42
+ * returned handler as a Tail Consumer; its structured logs are exported by the
43
+ * Tail Worker's own native OTLP logs destination.
44
+ */
45
+ export declare function createCloudflareAgentsTailHandler(options?: CloudflareAgentsObservabilityOptions): {
46
+ tail(events: CloudflareTailEvent[]): Promise<void>;
47
+ };
48
+ /** Explicit async-safe native custom span for Agent model/tool/RPC work. */
49
+ export declare function withCloudflareAgentSpan<T>(tracing: ExecutionContext | CloudflareNativeTracing, operation: string, attrs: Record<string, string | number | boolean>, fn: (span: Span) => T | Promise<T>): Promise<T>;
@@ -0,0 +1,110 @@
1
+ import { trace } from "@opentelemetry/api";
2
+ import { withCloudflareSpan, } from "./workers";
3
+ const SENSITIVE_KEY = /(?:authorization|cookie|password|secret|token|api[_-]?key|prompt|completion|content|message)/i;
4
+ const ERROR_EVENT = /(?::error|:failed|:stalled|:interrupted|:exhausted)$/;
5
+ function sanitize(value, extraKeys, depth = 0) {
6
+ if (depth > 5)
7
+ return "[truncated]";
8
+ if (value === null || typeof value === "boolean" || typeof value === "number")
9
+ return value;
10
+ if (typeof value === "string")
11
+ return value.length > 2_000 ? `${value.slice(0, 2_000)}…` : value;
12
+ if (Array.isArray(value))
13
+ return value.slice(0, 50).map((item) => sanitize(item, extraKeys, depth + 1));
14
+ if (typeof value === "object") {
15
+ const out = {};
16
+ for (const [key, item] of Object.entries(value).slice(0, 100)) {
17
+ out[key] = SENSITIVE_KEY.test(key) || extraKeys.has(key) ? "[redacted]" : sanitize(item, extraKeys, depth + 1);
18
+ }
19
+ return out;
20
+ }
21
+ return String(value);
22
+ }
23
+ function eventAttributes(event) {
24
+ const attrs = { "cloudflare.agent.event.type": event.type };
25
+ if (event.agent)
26
+ attrs["cloudflare.agent.class"] = event.agent;
27
+ if (event.name)
28
+ attrs["cloudflare.agent.name"] = event.name;
29
+ if (event.timestamp)
30
+ attrs["cloudflare.agent.event.timestamp_ms"] = event.timestamp;
31
+ for (const [key, value] of Object.entries(event.payload ?? {})) {
32
+ if (typeof value === "string" || typeof value === "number" || typeof value === "boolean") {
33
+ if (!SENSITIVE_KEY.test(key))
34
+ attrs[`cloudflare.agent.${key}`] = value;
35
+ }
36
+ }
37
+ return attrs;
38
+ }
39
+ /**
40
+ * Agents SDK `Observability` implementation. It emits one structured log for
41
+ * every lifecycle event and mirrors safe scalar metadata onto the active span.
42
+ * Native Workers log export then preserves trace correlation without an extra
43
+ * network request in the Agent/Durable Object request path.
44
+ */
45
+ export function createCloudflareAgentsObservability(options = {}) {
46
+ const include = options.include ? new Set(options.include) : undefined;
47
+ const redactKeys = new Set(options.redactKeys ?? []);
48
+ const logger = options.logger ?? ((level, value) => {
49
+ if (level === "error")
50
+ console.error(value);
51
+ else
52
+ console.log(value);
53
+ });
54
+ return {
55
+ emit(event) {
56
+ if (!event?.type || (include && !include.has(event.type)))
57
+ return;
58
+ const level = ERROR_EVENT.test(event.type) ? "error" : "info";
59
+ const safeEvent = sanitize(event, redactKeys);
60
+ const value = {
61
+ event: "cloudflare.agent",
62
+ ...options.attributes,
63
+ ...safeEvent,
64
+ severity: level,
65
+ };
66
+ logger(level, value);
67
+ const active = trace.getActiveSpan();
68
+ if (active?.isRecording())
69
+ active.addEvent(`cloudflare.agent.${event.type}`, {
70
+ ...options.attributes,
71
+ ...eventAttributes(event),
72
+ });
73
+ },
74
+ };
75
+ }
76
+ /**
77
+ * Tail Worker handler for production Agents diagnostics channels. Attach the
78
+ * returned handler as a Tail Consumer; its structured logs are exported by the
79
+ * Tail Worker's own native OTLP logs destination.
80
+ */
81
+ export function createCloudflareAgentsTailHandler(options = {}) {
82
+ const observability = createCloudflareAgentsObservability(options);
83
+ return {
84
+ async tail(events) {
85
+ for (const event of events) {
86
+ for (const diagnostic of event.diagnosticsChannelEvents ?? []) {
87
+ if (!diagnostic.channel?.startsWith("agents:"))
88
+ continue;
89
+ const message = diagnostic.message;
90
+ if (!message || typeof message !== "object")
91
+ continue;
92
+ const agentEvent = message;
93
+ observability.emit({
94
+ ...agentEvent,
95
+ type: agentEvent.type || diagnostic.channel.slice("agents:".length),
96
+ timestamp: agentEvent.timestamp ?? diagnostic.timestamp,
97
+ diagnostics_channel: diagnostic.channel,
98
+ });
99
+ }
100
+ }
101
+ },
102
+ };
103
+ }
104
+ /** Explicit async-safe native custom span for Agent model/tool/RPC work. */
105
+ export function withCloudflareAgentSpan(tracing, operation, attrs, fn) {
106
+ return withCloudflareSpan(tracing, `agent.${operation}`, {
107
+ "cloudflare.agent.operation": operation,
108
+ ...attrs,
109
+ }, fn);
110
+ }
package/dist/workers.d.ts CHANGED
@@ -2,6 +2,7 @@ import { type Span } from "@opentelemetry/api";
2
2
  import { type Context, type ContextManager } from "@opentelemetry/api";
3
3
  import { BasicTracerProvider, type ReadableSpan, type SpanExporter } from "@opentelemetry/sdk-trace-base";
4
4
  import { type HeystackOptions } from "./core.js";
5
+ import { type LLMGatewayConfig } from "./llm-enrich.js";
5
6
  declare const ExportResultCode: {
6
7
  readonly SUCCESS: 0;
7
8
  readonly FAILED: 1;
@@ -171,6 +172,17 @@ export interface WorkersConfig {
171
172
  * Defaults to `false` (no binding tracing). Consumed by a later task.
172
173
  */
173
174
  instrumentBindings?: boolean | string[];
175
+ /**
176
+ * Select the Workers tracing plane. `auto` (default) uses Cloudflare's native
177
+ * tracer when `ctx.tracing` is available and otherwise falls back to direct
178
+ * OTLP export. `native` prefers the platform tracer but still fails open when
179
+ * unavailable. `direct` always uses Heystack's fetch exporter.
180
+ *
181
+ * Native mode deliberately disables Heystack root/fetch/binding
182
+ * instrumentation: Cloudflare already emits those spans, and running both
183
+ * creates duplicate, disconnected trace trees.
184
+ */
185
+ nativeTracing?: "auto" | "native" | "direct";
174
186
  /**
175
187
  * Head-sampling configuration. When omitted (or `rate` >= 1), all traces are
176
188
  * kept. When `rate` < 1, a deterministic FNV-1a hash of the trace ID decides
@@ -192,20 +204,45 @@ export interface WorkersConfig {
192
204
  };
193
205
  /**
194
206
  * LLM/gen_ai capture for outbound calls to known providers (OpenAI, Anthropic,
195
- * CF AI Gateway, Google). Metadata (model, tokens, latency, finish_reason) is
196
- * always captured for detected providers.
207
+ * Mistral, Google, CF AI Gateway). Metadata (model, tokens, latency,
208
+ * finish_reason) is always captured for detected providers — including
209
+ * streamed (`text/event-stream`) responses, whose usage is lifted from the
210
+ * stream without buffering content. Works on both tracing planes: as a CLIENT
211
+ * span in direct mode and as a native `<operation> <provider>` span (e.g.
212
+ * `chat mistral`) under Cloudflare native tracing.
213
+ *
214
+ * `gateways` teaches the detector about AI gateways / proxies you call
215
+ * instead of the provider host (Wafer, a corporate proxy, …): each entry
216
+ * names the gateway host and a path pattern whose `:provider` segment carries
217
+ * the provider token (`openai | anthropic | google | gemini | mistral |
218
+ * cloudflare | workers-ai`). Resolved calls get `gen_ai.system=<provider>` +
219
+ * `heystack.ai.gateway=<host>`. Example for Wafer:
220
+ * `gateways: [{ hostSuffix: "wafersecurity.ai", pathPattern: "/p/:tenant/:provider" }]`
197
221
  *
198
222
  * Set `captureContent: true` to also capture prompt/completion text (off by
199
223
  * default) — strongly recommended for AI-app RCA. Provide `redact` to scrub
200
224
  * sensitive text before it leaves the Worker. `maxContentChars` caps the length
201
- * of each captured value (default 8000 characters).
225
+ * of each captured value (default 8000 characters). Content is never captured
226
+ * from streamed responses.
202
227
  */
203
228
  ai?: {
204
229
  captureContent?: boolean;
205
230
  redact?: (text: string) => string;
206
231
  maxContentChars?: number;
232
+ gateways?: LLMGatewayConfig[];
207
233
  };
208
234
  }
235
+ /** Minimal public surface exposed by Cloudflare's Workers custom-spans API. */
236
+ export interface CloudflareNativeSpan {
237
+ readonly isTraced: boolean;
238
+ setAttribute(key: string, value: string | number | boolean | undefined): void;
239
+ end(): void;
240
+ }
241
+ /** `ExecutionContext.tracing` without importing the `cloudflare:workers` module. */
242
+ export interface CloudflareNativeTracing {
243
+ enterSpan<T>(name: string, callback: (span: CloudflareNativeSpan) => T): T;
244
+ startActiveSpan<T>(name: string, callback: (span: CloudflareNativeSpan) => T): T;
245
+ }
209
246
  /**
210
247
  * A `BasicTracerProvider` with the underlying `HeystackSpanExporter` attached so
211
248
  * flush paths can drain its in-flight fetches directly (the OTel processor's
@@ -388,9 +425,30 @@ export declare function instrument<H extends WorkerHandler<any>>(handler: H, con
388
425
  */
389
426
  export declare function withSpan<T>(name: string, fn: (span: Span) => T | Promise<T>): Promise<T>;
390
427
  export declare function withSpan<T>(name: string, attrs: Record<string, string | number | boolean>, fn: (span: Span) => T | Promise<T>): Promise<T>;
428
+ /**
429
+ * Create a Cloudflare-native custom span using an explicit ExecutionContext.
430
+ * Prefer this form in code that calls `withSpan` after an `await` on a Worker
431
+ * without `nodejs_compat`: the explicit context cannot be lost across async
432
+ * boundaries. It falls back to the regular OTel tracer outside Cloudflare.
433
+ */
434
+ export declare function withCloudflareSpan<T>(ctx: ExecutionContext | CloudflareNativeTracing, name: string, fn: (span: Span) => T | Promise<T>): Promise<T>;
435
+ export declare function withCloudflareSpan<T>(ctx: ExecutionContext | CloudflareNativeTracing, name: string, attrs: Record<string, string | number | boolean>, fn: (span: Span) => T | Promise<T>): Promise<T>;
391
436
  /**
392
437
  * Add a named event (with optional attributes) to the currently-active span.
393
438
  * No-op when no span is active.
394
439
  */
395
440
  export declare function addEvent(name: string, attrs?: Record<string, string | number | boolean>): void;
396
441
  export type { Span };
442
+ /**
443
+ * `gen_ai.system` + `gen_ai.request.*` from an LLM request body (model,
444
+ * max_tokens, temperature). Alias of the internal request mapper.
445
+ */
446
+ export { llmRequestAttrs as genAiRequestAttrs } from "./llm-enrich.js";
447
+ /**
448
+ * `gen_ai.response.*` + `gen_ai.usage.*` from an LLM response object. Understands
449
+ * OpenAI/Mistral (`usage.prompt_tokens`/`completion_tokens`), Anthropic
450
+ * (`usage.input_tokens`/`output_tokens`) and Google (`usageMetadata`) shapes —
451
+ * Workers AI results (`usage.prompt_tokens`/`completion_tokens`) included.
452
+ */
453
+ export { llmResponseAttrs as genAiResponseAttrs } from "./llm-enrich.js";
454
+ export type { LLMGatewayConfig, LLMProvider } from "./llm-enrich.js";