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.
- package/dist/index.cjs +458 -0
- package/dist/index.js +430 -1
- package/package.json +15 -10
- package/src/core/dispatcher.ts +212 -0
- package/src/core/tracer.ts +171 -0
- package/src/core/types.ts +73 -0
- package/src/diff.ts +181 -0
- package/src/index.ts +20 -33
- package/src/utils/cost.ts +104 -0
- package/src/wrappers/openai.ts +193 -0
- package/tsup.config.ts +5 -10
- package/dist/index.d.mts +0 -14
- package/dist/index.d.ts +0 -14
- package/dist/index.mjs +0 -1
- package/tsconfig.json +0 -23
|
@@ -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
|
-
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
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
|
-
}
|