0xtrace 1.0.0 → 1.0.1

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,193 @@
1
+ // ─────────────────────────────────────────────────────────────────────────────
2
+ // packages/sdk/src/wrappers/openai.ts
3
+ //
4
+ // Wraps an OpenAI client instance in a deeply nested Proxy that intercepts
5
+ // `chat.completions.create`, captures telemetry, and fires it non-blocking.
6
+ //
7
+ // Key guarantees:
8
+ // 1. The original OpenAI types are 100% preserved — callers see no diffs.
9
+ // 2. Streaming responses (`stream: true`) are fully supported via an async
10
+ // generator that transparently yields every chunk unchanged.
11
+ // 3. Telemetry is fired via the microtask queue — zero latency added.
12
+ // 4. Nothing is monkey-patched; the original client is never mutated.
13
+ // ─────────────────────────────────────────────────────────────────────────────
14
+
15
+ import OpenAI from "openai";
16
+ import type {
17
+ ChatCompletionCreateParamsNonStreaming,
18
+ ChatCompletionCreateParamsStreaming,
19
+ ChatCompletion,
20
+ } from "openai/resources/chat/completions";
21
+ import type { Stream } from "openai/streaming";
22
+ import type { ChatCompletionChunk } from "openai/resources";
23
+ import type { Tracer } from "../core/tracer";
24
+ import type { ChatMessage } from "../core/types";
25
+
26
+ // ── Type helpers ──────────────────────────────────────────────────────────────
27
+
28
+ type CreateParams =
29
+ | ChatCompletionCreateParamsNonStreaming
30
+ | ChatCompletionCreateParamsStreaming;
31
+
32
+ // ── Main export ───────────────────────────────────────────────────────────────
33
+
34
+ /**
35
+ * Wraps an OpenAI client with a transparent telemetry layer.
36
+ *
37
+ * @param client The original `new OpenAI(...)` instance.
38
+ * @param tracer A configured `Tracer` instance (owns the session + dispatch).
39
+ * @returns A Proxy of the client — drop-in replacement, same types.
40
+ *
41
+ * @example
42
+ * import OpenAI from "openai";
43
+ * import { Tracer, wrapOpenAI } from "@prompt-tracer/sdk";
44
+ *
45
+ * const openai = new OpenAI({ apiKey: process.env.OPENAI_API_KEY });
46
+ * const tracer = new Tracer({ ingestUrl: "https://your-app.com/api/ingest" });
47
+ * const ai = wrapOpenAI(openai, tracer);
48
+ *
49
+ * // Use exactly like the original client — streaming, tools, everything works.
50
+ * const res = await ai.chat.completions.create({ model: "gpt-4o", messages });
51
+ */
52
+ export function wrapOpenAI(client: OpenAI, tracer: Tracer): OpenAI {
53
+ return new Proxy(client, {
54
+ get(target, prop, receiver) {
55
+ // ── Intercept .chat ───────────────────────────────────────────────────
56
+ if (prop === "chat") {
57
+ return new Proxy(target.chat, {
58
+ get(chatTarget, chatProp, chatReceiver) {
59
+ // ── Intercept .chat.completions ─────────────────────────────────
60
+ if (chatProp === "completions") {
61
+ return new Proxy(chatTarget.completions, {
62
+ get(compTarget, compProp, compReceiver) {
63
+ // ── Intercept .chat.completions.create ────────────────────
64
+ if (compProp === "create") {
65
+ return _makeCreateInterceptor(compTarget, tracer);
66
+ }
67
+ // All other completions methods (e.g. .stream()) pass through
68
+ return Reflect.get(compTarget, compProp, compReceiver);
69
+ },
70
+ });
71
+ }
72
+ return Reflect.get(chatTarget, chatProp, chatReceiver);
73
+ },
74
+ });
75
+ }
76
+ // All other top-level methods (embeddings, images, etc.) pass through
77
+ return Reflect.get(target, prop, receiver);
78
+ },
79
+ });
80
+ }
81
+
82
+ // ── Interceptor factory ───────────────────────────────────────────────────────
83
+
84
+ /**
85
+ * Returns the replacement `create` function that wraps the original.
86
+ * Extracted so the Proxy `get` handler stays readable.
87
+ */
88
+ function _makeCreateInterceptor(
89
+ compTarget: OpenAI["chat"]["completions"],
90
+ tracer: Tracer
91
+ ) {
92
+ // Overloaded signature mirrors the OpenAI SDK exactly so TypeScript callers
93
+ // see the correct return type based on whether `stream` is true.
94
+ async function create(
95
+ params: ChatCompletionCreateParamsNonStreaming
96
+ ): Promise<ChatCompletion>;
97
+ async function create(
98
+ params: ChatCompletionCreateParamsStreaming
99
+ ): Promise<Stream<ChatCompletionChunk>>;
100
+ async function create(params: CreateParams): Promise<unknown> {
101
+ const startMs = Date.now();
102
+
103
+ if (params.stream === true) {
104
+ // ── Streaming path ────────────────────────────────────────────────────
105
+ // We must return an async generator so the caller's `for await` loop
106
+ // works identically to the original SDK.
107
+ const stream = await (
108
+ compTarget.create as (
109
+ p: ChatCompletionCreateParamsStreaming
110
+ ) => Promise<Stream<ChatCompletionChunk>>
111
+ )(params as ChatCompletionCreateParamsStreaming);
112
+
113
+ return _wrapStream(stream, params, startMs, tracer);
114
+ }
115
+
116
+ // ── Non-streaming path ────────────────────────────────────────────────────
117
+ const result = await (
118
+ compTarget.create as (
119
+ p: ChatCompletionCreateParamsNonStreaming
120
+ ) => Promise<ChatCompletion>
121
+ )(params as ChatCompletionCreateParamsNonStreaming);
122
+
123
+ const latencyMs = Date.now() - startMs;
124
+
125
+ // Fire telemetry onto the microtask queue — never blocks the caller.
126
+ tracer.captureAsync({
127
+ prompt: params.messages as ChatMessage[],
128
+ response: result.choices[0]?.message?.content ?? "",
129
+ model: params.model,
130
+ tokensIn: result.usage?.prompt_tokens,
131
+ tokensOut: result.usage?.completion_tokens,
132
+ latencyMs,
133
+ isStream: false,
134
+ });
135
+
136
+ return result;
137
+ }
138
+
139
+ return create;
140
+ }
141
+
142
+ // ── Stream wrapper ────────────────────────────────────────────────────────────
143
+
144
+ /**
145
+ * Wraps an OpenAI streaming response in an async generator that:
146
+ * 1. Yields every chunk to the caller unchanged.
147
+ * 2. Reconstructs the full text and counts chunks.
148
+ * 3. Fires telemetry after the last chunk via the microtask queue.
149
+ *
150
+ * The returned generator preserves the `Symbol.asyncIterator` contract so
151
+ * `for await (const chunk of stream)` works exactly as before.
152
+ */
153
+ async function* _wrapStream(
154
+ stream: Stream<ChatCompletionChunk>,
155
+ params: CreateParams,
156
+ startMs: number,
157
+ tracer: Tracer
158
+ ): AsyncGenerator<ChatCompletionChunk> {
159
+ let fullContent = "";
160
+ let chunkCount = 0;
161
+ let promptTokens: number | undefined;
162
+
163
+ try {
164
+ for await (const chunk of stream) {
165
+ // Capture prompt tokens if the first chunk carries usage data
166
+ // (available when `stream_options: { include_usage: true }` is set).
167
+ if (chunk.usage?.prompt_tokens !== undefined) {
168
+ promptTokens = chunk.usage.prompt_tokens;
169
+ }
170
+
171
+ const delta = chunk.choices[0]?.delta?.content ?? "";
172
+ fullContent += delta;
173
+ chunkCount += 1;
174
+
175
+ yield chunk; // ← caller gets every chunk unmodified, zero delay
176
+ }
177
+ } finally {
178
+ // `finally` runs whether the caller broke out early or read everything.
179
+ const latencyMs = Date.now() - startMs;
180
+
181
+ // Schedule telemetry on the microtask queue so it fires after the
182
+ // caller's current `for await` iteration completes.
183
+ tracer.captureAsync({
184
+ prompt: params.messages as ChatMessage[],
185
+ response: fullContent,
186
+ model: params.model,
187
+ tokensIn: promptTokens, // exact if include_usage was set
188
+ tokensOut: chunkCount, // approximation: 1 chunk ≈ 1 token
189
+ latencyMs,
190
+ isStream: true,
191
+ });
192
+ }
193
+ }
package/tsup.config.ts CHANGED
@@ -1,12 +1,7 @@
1
- // If the 'tsup' types are not available in this environment, suppress the import error
2
- // to allow building without ambient module augmentation files.
3
- // @ts-ignore: module 'tsup' may be missing in some environments
4
- import { defineConfig } from "tsup";
5
-
6
- export default (defineConfig as any)({
7
- entry: ["src/index.ts"],
8
- format: ["cjs", "esm"],
9
- dts: true,
1
+ import { defineConfig } from 'tsup';
2
+ export default defineConfig({
3
+ entry: ['src/index.ts'],
4
+ format: ['cjs', 'esm'],
5
+ dts: false,
10
6
  clean: true,
11
- minify: true
12
7
  });
package/dist/index.d.mts DELETED
@@ -1,14 +0,0 @@
1
- interface TracePayload {
2
- sessionId: string;
3
- stepIndex: number;
4
- model: string;
5
- messages: any[];
6
- latencyMs: number;
7
- tokensIn: number;
8
- tokensOut: number;
9
- estimatedCostUsd: number;
10
- isStream?: boolean;
11
- }
12
- declare function traceLlmCall(payload: TracePayload): Promise<void>;
13
-
14
- export { type TracePayload, traceLlmCall };
package/dist/index.d.ts DELETED
@@ -1,14 +0,0 @@
1
- interface TracePayload {
2
- sessionId: string;
3
- stepIndex: number;
4
- model: string;
5
- messages: any[];
6
- latencyMs: number;
7
- tokensIn: number;
8
- tokensOut: number;
9
- estimatedCostUsd: number;
10
- isStream?: boolean;
11
- }
12
- declare function traceLlmCall(payload: TracePayload): Promise<void>;
13
-
14
- export { type TracePayload, traceLlmCall };
package/dist/index.mjs DELETED
@@ -1 +0,0 @@
1
- async function o(n){let r=process.env.UPSTASH_REDIS_REST_URL,t=process.env.UPSTASH_REDIS_REST_TOKEN;if(!(!r||!t))try{fetch(`${r}/lpush/0xtrace_queue`,{method:"POST",headers:{Authorization:`Bearer ${t}`,"Content-Type":"application/json"},body:JSON.stringify({...n,timestamp:new Date().toISOString()})}).catch(e=>console.error(e))}catch(e){console.error(e)}}export{o as traceLlmCall};
package/tsconfig.json DELETED
@@ -1,23 +0,0 @@
1
- {
2
- "compilerOptions": {
3
- "target": "es2022",
4
- "module": "ESNext",
5
- "declaration": true,
6
- "strict": true,
7
- "esModuleInterop": true,
8
- "skipLibCheck": true,
9
- "forceConsistentCasingInFileNames": true,
10
- "moduleResolution": "bundler",
11
- "types": [
12
- "node"
13
- ],
14
- "ignoreDeprecations": "6.0"
15
- },
16
- "include": [
17
- "src/**/*"
18
- ],
19
- "exclude": [
20
- "node_modules",
21
- "dist"
22
- ]
23
- }