0xtrace 1.0.3 → 1.0.5

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.
@@ -0,0 +1,57 @@
1
+ import type { IDispatcher, TracePayload } from "./types";
2
+ export interface DispatcherOptions {
3
+ ingestUrl: string;
4
+ apiKey?: string;
5
+ timeoutMs?: number;
6
+ batchSize?: number;
7
+ flushIntervalMs?: number;
8
+ onError?: (error: Error, payloads: TracePayload[]) => void;
9
+ }
10
+ export declare class Dispatcher implements IDispatcher {
11
+ private readonly ingestUrl;
12
+ private readonly apiKey;
13
+ private readonly timeoutMs;
14
+ private readonly batchSize;
15
+ private readonly onError;
16
+ /** The in-memory buffer accumulating payloads between flushes. */
17
+ private buffer;
18
+ /** The NodeJS/browser timer handle for the periodic flush. */
19
+ private flushTimer;
20
+ /** Tracks all in-flight fetch Promises so flush() can await them. */
21
+ private inFlight;
22
+ constructor(opts: DispatcherOptions);
23
+ /**
24
+ * Accepts a payload and schedules delivery non-blocking via the microtask
25
+ * queue. The caller returns immediately; the POST happens asynchronously.
26
+ */
27
+ send(payload: TracePayload): void;
28
+ /**
29
+ * Waits for all in-flight requests and flushes any remaining buffered
30
+ * payloads. Call this in tests or on process shutdown.
31
+ *
32
+ * @example
33
+ * process.on('SIGTERM', () => tracer.flush());
34
+ */
35
+ flush(): Promise<void>;
36
+ /**
37
+ * Stops the periodic flush timer and flushes remaining payloads.
38
+ * Call when the Tracer instance is being torn down.
39
+ */
40
+ destroy(): Promise<void>;
41
+ /**
42
+ * Atomically snapshots and clears the buffer, then initiates an async
43
+ * POST. Multiple concurrent drains are safe — each works on its own slice.
44
+ */
45
+ private _drainBuffer;
46
+ /**
47
+ * Attempts to POST a batch to the ingest endpoint.
48
+ * Retries up to MAX_RETRY_ATTEMPTS times with exponential back-off.
49
+ * Only retries on network errors or 5xx responses.
50
+ */
51
+ private _sendWithRetry;
52
+ /**
53
+ * Performs the raw HTTP POST with an AbortController timeout.
54
+ * Throws on network failure or non-2xx status.
55
+ */
56
+ private _post;
57
+ }
@@ -0,0 +1,55 @@
1
+ import type { RawCapturePayload, TracerOptions, IDispatcher } from "./types";
2
+ export declare class Tracer {
3
+ /** Groups all LLM calls in this agent run. */
4
+ readonly sessionId: string;
5
+ /** Caller-supplied arbitrary metadata attached to every payload. */
6
+ private readonly metadata;
7
+ /** Whether telemetry is active (can be disabled via options). */
8
+ private readonly enabled;
9
+ /** Delivery engine — injectable for unit-testing. */
10
+ private readonly dispatcher;
11
+ /**
12
+ * Monotonically increasing step counter.
13
+ * Step 1 → first call in the session (triggers full snapshot in the DB).
14
+ * Step N → subsequent calls (store diff only).
15
+ */
16
+ private stepCounter;
17
+ constructor(opts: TracerOptions, dispatcher?: IDispatcher);
18
+ /**
19
+ * The method called by every SDK wrapper after intercepting an LLM call.
20
+ *
21
+ * Design contract:
22
+ * - NEVER awaited by the wrapper; fire-and-forget on microtask queue.
23
+ * - Returns void so the wrapper cannot accidentally `await` it.
24
+ *
25
+ * @example
26
+ * // Inside wrappers/openai.ts — after receiving the result:
27
+ * tracer.captureAsync({ prompt, response, model, tokensIn, tokensOut, latencyMs, isStream });
28
+ */
29
+ captureAsync(raw: RawCapturePayload): void;
30
+ /**
31
+ * Returns the step index the *next* call will be assigned.
32
+ * Useful for callers who need to know if this is step 1 (full snapshot)
33
+ * vs. a later step (diff only) before making the LLM call.
34
+ */
35
+ get nextStepIndex(): number;
36
+ /**
37
+ * Waits for all buffered and in-flight payloads to be delivered.
38
+ * Call before process exit or at the end of integration tests.
39
+ *
40
+ * @example
41
+ * afterAll(async () => { await tracer.flush(); });
42
+ */
43
+ flush(): Promise<void>;
44
+ /**
45
+ * Takes a raw capture from the wrapper and enriches it with:
46
+ * - a unique callId
47
+ * - the session's sessionId
48
+ * - a monotonic stepIndex
49
+ * - ISO-8601 timestamp
50
+ * - USD cost estimate
51
+ * - SDK version string
52
+ * - caller metadata
53
+ */
54
+ private _enrich;
55
+ }
@@ -0,0 +1,64 @@
1
+ /** A single message in an OpenAI-compatible chat conversation. */
2
+ export interface ChatMessage {
3
+ role: "system" | "user" | "assistant" | "tool" | "function";
4
+ content: string | null;
5
+ name?: string;
6
+ tool_call_id?: string;
7
+ }
8
+ /** Raw data captured at the intercept point, before any enrichment. */
9
+ export interface RawCapturePayload {
10
+ /** The messages array sent to the model. */
11
+ prompt: ChatMessage[] | readonly ChatMessage[];
12
+ /** The text content of the completion (reconstructed for streams). */
13
+ response: string;
14
+ /** Model string exactly as passed by the caller e.g. "gpt-4o". */
15
+ model: string;
16
+ /** Prompt tokens from usage object. Undefined for streams (not available). */
17
+ tokensIn: number | undefined;
18
+ /** Completion tokens. For streams this is an approximation (chunk count). */
19
+ tokensOut: number | undefined;
20
+ /** Wall-clock ms from request start to last byte received. */
21
+ latencyMs: number;
22
+ /** Whether the call used server-sent streaming. */
23
+ isStream: boolean;
24
+ }
25
+ /** The fully enriched payload that gets pushed to the ingest queue. */
26
+ export interface TracePayload extends RawCapturePayload {
27
+ /** SDK-generated UUID for this individual LLM call. */
28
+ callId: string;
29
+ /** Session/trace ID grouping multiple calls in one agent run.
30
+ * Set via TracerOptions.sessionId or auto-generated per Tracer instance. */
31
+ sessionId: string;
32
+ /** Step index within the session (1-based, incremented per capture). */
33
+ stepIndex: number;
34
+ /** ISO-8601 timestamp of the call start. */
35
+ timestamp: string;
36
+ /** Estimated USD cost for this call. */
37
+ estimatedCostUsd: number;
38
+ /** Version string of the SDK emitting this payload. */
39
+ sdkVersion: string;
40
+ }
41
+ /** Options passed when constructing a Tracer instance. */
42
+ export interface TracerOptions {
43
+ /** Full URL of your Next.js ingest endpoint.
44
+ * e.g. "https://your-app.vercel.app/api/ingest" */
45
+ ingestUrl: string;
46
+ apiKey?: string;
47
+ /** Optional session ID to group multiple calls into one trace. If omitted,
48
+ /** Groups multiple LLM calls into one logical agent run.
49
+ * Auto-generated (UUID v4) if omitted. */
50
+ sessionId?: string;
51
+ /** Attach arbitrary key/value metadata to every payload (e.g. userId, env). */
52
+ metadata?: Record<string, string>;
53
+ /** Max ms to wait for the ingest POST before aborting. Default: 5000. */
54
+ timeoutMs?: number;
55
+ /** Called when the ingest POST fails. Defaults to console.warn. */
56
+ onError?: (error: Error, payload: TracePayload) => void;
57
+ /** Set false to completely disable telemetry (e.g. in unit tests). Default: true */
58
+ enabled?: boolean;
59
+ }
60
+ /** Minimal interface the Dispatcher must satisfy — useful for testing. */
61
+ export interface IDispatcher {
62
+ send(payload: TracePayload): void;
63
+ flush(): Promise<void>;
64
+ }
package/dist/diff.d.ts ADDED
@@ -0,0 +1,74 @@
1
+ import type { ChatMessage } from "./core/types";
2
+ /** A single entry in the diff — describes ONE message's change. */
3
+ export type DiffOperation = {
4
+ op: "add";
5
+ index: number;
6
+ message: ChatMessage;
7
+ } | {
8
+ op: "remove";
9
+ index: number;
10
+ } | {
11
+ op: "keep";
12
+ index: number;
13
+ };
14
+ /** The payload stored in prompt_snapshots.diff_from_previous */
15
+ export interface MessageDiff {
16
+ /** Only the add/remove operations (keeps are omitted to save bytes). */
17
+ operations: Array<{
18
+ op: "add";
19
+ index: number;
20
+ message: ChatMessage;
21
+ } | {
22
+ op: "remove";
23
+ index: number;
24
+ }>;
25
+ /** Net token change: positive = context grew, negative = messages pruned. */
26
+ tokenDelta: number;
27
+ /** How many messages were added in this step. */
28
+ added: number;
29
+ /** How many messages were removed in this step. */
30
+ removed: number;
31
+ }
32
+ /**
33
+ * Rough token estimator — 1 token ≈ 4 characters (GPT rule of thumb).
34
+ * The SDK does not run a full tokenizer to stay dependency-free.
35
+ * The backend can re-calculate with tiktoken if needed.
36
+ */
37
+ export declare function estimateTokens(messages: readonly ChatMessage[]): number;
38
+ /**
39
+ * Computes the minimal diff between `prev` and `curr` message arrays.
40
+ *
41
+ * Algorithm: O(n) two-pointer walk.
42
+ * 1. Build a Set of keys in `prev` for O(1) lookup.
43
+ * 2. Walk `curr` — any message not in `prev` is an ADD.
44
+ * 3. Walk `prev` — any message not in `curr` is a REMOVE.
45
+ *
46
+ * This is sufficient for 99% of real agent patterns where the context
47
+ * array only ever has messages appended (never reordered mid-stream).
48
+ * For adversarial reordering, swap to Myers diff.
49
+ *
50
+ * @example
51
+ * const diff = computeMessageDiff(step1Messages, step2Messages);
52
+ * // { operations: [{ op: "add", index: 3, message: {...} }], tokenDelta: 42, added: 1, removed: 0 }
53
+ */
54
+ export declare function computeMessageDiff(prev: readonly ChatMessage[], curr: readonly ChatMessage[]): MessageDiff;
55
+ /**
56
+ * Applies a stored diff forward onto a base message array.
57
+ * The frontend calls this to reconstruct the full message array for step N:
58
+ *
59
+ * const step1 = fullSnapshot; // stored in DB for step 1
60
+ * const step2 = applyDiff(step1, diff); // reconstructed from diff
61
+ * const step3 = applyDiff(step2, diff); // and so on...
62
+ *
63
+ * @throws {Error} if the diff references an out-of-bounds index.
64
+ */
65
+ export declare function applyDiff(base: readonly ChatMessage[], diff: MessageDiff): ChatMessage[];
66
+ /**
67
+ * Replays an ordered series of diffs from a base snapshot.
68
+ * Use this when you need to reconstruct every step in a session at once.
69
+ *
70
+ * @example
71
+ * const steps = replayDiffs(step1Snapshot, [diff2, diff3, diff4]);
72
+ * // steps[0] === step1, steps[1] === step2, steps[2] === step3, steps[3] === step4
73
+ */
74
+ export declare function replayDiffs(baseSnapshot: readonly ChatMessage[], diffs: MessageDiff[]): ChatMessage[][];
@@ -0,0 +1,6 @@
1
+ export { Tracer } from "./core/tracer";
2
+ export { Dispatcher } from "./core/dispatcher";
3
+ export { wrapOpenAI } from "./wrappers/openai";
4
+ export { calcCostUsd, formatCostUsd } from "./utils/cost";
5
+ export type { ChatMessage, RawCapturePayload, TracePayload, TracerOptions, IDispatcher, } from "./core/types";
6
+ export type { DispatcherOptions } from "./core/dispatcher";
@@ -0,0 +1,21 @@
1
+ export interface CalcCostParams {
2
+ model: string;
3
+ tokensIn: number;
4
+ tokensOut: number;
5
+ }
6
+ /**
7
+ * Returns the estimated USD cost for a single LLM call.
8
+ * Returns 0 for unrecognised models rather than throwing, so the SDK
9
+ * never crashes user code due to a missing pricing entry.
10
+ *
11
+ * @example
12
+ * calcCostUsd({ model: "gpt-4o", tokensIn: 1000, tokensOut: 500 })
13
+ * // → 0.00750
14
+ */
15
+ export declare function calcCostUsd({ model, tokensIn, tokensOut }: CalcCostParams): number;
16
+ /**
17
+ * Formats a USD cost as a human-readable string.
18
+ * @example formatCost(0.0075) → "$0.0075"
19
+ * @example formatCost(0.00000120) → "$0.0000012"
20
+ */
21
+ export declare function formatCostUsd(usd: number): string;
@@ -0,0 +1,21 @@
1
+ import OpenAI from "openai";
2
+ import type { Tracer } from "../core/tracer";
3
+ /**
4
+ * Wraps an OpenAI client with a transparent telemetry layer.
5
+ *
6
+ * @param client The original `new OpenAI(...)` instance.
7
+ * @param tracer A configured `Tracer` instance (owns the session + dispatch).
8
+ * @returns A Proxy of the client — drop-in replacement, same types.
9
+ *
10
+ * @example
11
+ * import OpenAI from "openai";
12
+ * import { Tracer, wrapOpenAI } from "@prompt-tracer/sdk";
13
+ *
14
+ * const openai = new OpenAI({ apiKey: process.env.OPENAI_API_KEY });
15
+ * const tracer = new Tracer({ ingestUrl: "https://your-app.com/api/ingest" });
16
+ * const ai = wrapOpenAI(openai, tracer);
17
+ *
18
+ * // Use exactly like the original client — streaming, tools, everything works.
19
+ * const res = await ai.chat.completions.create({ model: "gpt-4o", messages });
20
+ */
21
+ export declare function wrapOpenAI(client: OpenAI, tracer: Tracer): OpenAI;
package/package.json CHANGED
@@ -1,12 +1,14 @@
1
1
  {
2
2
  "name": "0xtrace",
3
- "version": "1.0.3",
3
+ "version": "1.0.5",
4
4
  "description": "Proxy-based LLM telemetry SDK for 0xtrace",
5
5
  "type": "module",
6
6
  "main": "./dist/index.cjs",
7
7
  "module": "./dist/index.js",
8
+ "types": "./dist/index.d.ts",
8
9
  "exports": {
9
10
  ".": {
11
+ "types": "./dist/index.d.ts",
10
12
  "import": "./dist/index.js",
11
13
  "require": "./dist/index.cjs"
12
14
  }
@@ -15,7 +17,7 @@
15
17
  "dist"
16
18
  ],
17
19
  "scripts": {
18
- "build": "tsup"
20
+ "build": "tsup && tsc --project tsconfig.build.json"
19
21
  },
20
22
  "dependencies": {
21
23
  "diff": "^9.0.0",