0xtrace 1.0.0 → 1.0.2
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 +20 -11
- package/dist/index.d.mts +0 -14
- package/dist/index.d.ts +0 -14
- package/dist/index.mjs +0 -1
- package/src/index.ts +0 -37
- package/tsconfig.json +0 -23
- package/tsup.config.ts +0 -12
package/dist/index.cjs
ADDED
|
@@ -0,0 +1,458 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __defProp = Object.defineProperty;
|
|
3
|
+
var __defProps = Object.defineProperties;
|
|
4
|
+
var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
|
|
5
|
+
var __getOwnPropDescs = Object.getOwnPropertyDescriptors;
|
|
6
|
+
var __getOwnPropNames = Object.getOwnPropertyNames;
|
|
7
|
+
var __getOwnPropSymbols = Object.getOwnPropertySymbols;
|
|
8
|
+
var __hasOwnProp = Object.prototype.hasOwnProperty;
|
|
9
|
+
var __propIsEnum = Object.prototype.propertyIsEnumerable;
|
|
10
|
+
var __knownSymbol = (name, symbol) => (symbol = Symbol[name]) ? symbol : /* @__PURE__ */ Symbol.for("Symbol." + name);
|
|
11
|
+
var __defNormalProp = (obj, key, value) => key in obj ? __defProp(obj, key, { enumerable: true, configurable: true, writable: true, value }) : obj[key] = value;
|
|
12
|
+
var __spreadValues = (a, b) => {
|
|
13
|
+
for (var prop in b || (b = {}))
|
|
14
|
+
if (__hasOwnProp.call(b, prop))
|
|
15
|
+
__defNormalProp(a, prop, b[prop]);
|
|
16
|
+
if (__getOwnPropSymbols)
|
|
17
|
+
for (var prop of __getOwnPropSymbols(b)) {
|
|
18
|
+
if (__propIsEnum.call(b, prop))
|
|
19
|
+
__defNormalProp(a, prop, b[prop]);
|
|
20
|
+
}
|
|
21
|
+
return a;
|
|
22
|
+
};
|
|
23
|
+
var __spreadProps = (a, b) => __defProps(a, __getOwnPropDescs(b));
|
|
24
|
+
var __export = (target, all) => {
|
|
25
|
+
for (var name in all)
|
|
26
|
+
__defProp(target, name, { get: all[name], enumerable: true });
|
|
27
|
+
};
|
|
28
|
+
var __copyProps = (to, from, except, desc) => {
|
|
29
|
+
if (from && typeof from === "object" || typeof from === "function") {
|
|
30
|
+
for (let key of __getOwnPropNames(from))
|
|
31
|
+
if (!__hasOwnProp.call(to, key) && key !== except)
|
|
32
|
+
__defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
|
|
33
|
+
}
|
|
34
|
+
return to;
|
|
35
|
+
};
|
|
36
|
+
var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
|
|
37
|
+
var __await = function(promise, isYieldStar) {
|
|
38
|
+
this[0] = promise;
|
|
39
|
+
this[1] = isYieldStar;
|
|
40
|
+
};
|
|
41
|
+
var __asyncGenerator = (__this, __arguments, generator) => {
|
|
42
|
+
var resume = (k, v, yes, no) => {
|
|
43
|
+
try {
|
|
44
|
+
var x = generator[k](v), isAwait = (v = x.value) instanceof __await, done = x.done;
|
|
45
|
+
Promise.resolve(isAwait ? v[0] : v).then((y) => isAwait ? resume(k === "return" ? k : "next", v[1] ? { done: y.done, value: y.value } : y, yes, no) : yes({ value: y, done })).catch((e) => resume("throw", e, yes, no));
|
|
46
|
+
} catch (e) {
|
|
47
|
+
no(e);
|
|
48
|
+
}
|
|
49
|
+
}, method = (k, call, wait, clear) => it[k] = (x) => (call = new Promise((yes, no, run) => (run = () => resume(k, x, yes, no), q ? q.then(run) : run())), clear = () => q === wait && (q = 0), q = wait = call.then(clear, clear), call), q, it = {};
|
|
50
|
+
return generator = generator.apply(__this, __arguments), it[__knownSymbol("asyncIterator")] = () => it, method("next"), method("throw"), method("return"), it;
|
|
51
|
+
};
|
|
52
|
+
var __forAwait = (obj, it, method) => (it = obj[__knownSymbol("asyncIterator")]) ? it.call(obj) : (obj = obj[__knownSymbol("iterator")](), it = {}, method = (key, fn) => (fn = obj[key]) && (it[key] = (arg) => new Promise((yes, no, done) => (arg = fn.call(obj, arg), done = arg.done, Promise.resolve(arg.value).then((value) => yes({ value, done }), no)))), method("next"), method("return"), it);
|
|
53
|
+
|
|
54
|
+
// src/index.ts
|
|
55
|
+
var index_exports = {};
|
|
56
|
+
__export(index_exports, {
|
|
57
|
+
Dispatcher: () => Dispatcher,
|
|
58
|
+
Tracer: () => Tracer,
|
|
59
|
+
calcCostUsd: () => calcCostUsd,
|
|
60
|
+
formatCostUsd: () => formatCostUsd,
|
|
61
|
+
wrapOpenAI: () => wrapOpenAI
|
|
62
|
+
});
|
|
63
|
+
module.exports = __toCommonJS(index_exports);
|
|
64
|
+
|
|
65
|
+
// src/core/dispatcher.ts
|
|
66
|
+
var DEFAULT_BATCH_SIZE = 10;
|
|
67
|
+
var DEFAULT_FLUSH_MS = 2e3;
|
|
68
|
+
var DEFAULT_TIMEOUT_MS = 5e3;
|
|
69
|
+
var MAX_RETRY_ATTEMPTS = 3;
|
|
70
|
+
var BASE_RETRY_DELAY_MS = 200;
|
|
71
|
+
var Dispatcher = class {
|
|
72
|
+
constructor(opts) {
|
|
73
|
+
/** The in-memory buffer accumulating payloads between flushes. */
|
|
74
|
+
this.buffer = [];
|
|
75
|
+
/** The NodeJS/browser timer handle for the periodic flush. */
|
|
76
|
+
this.flushTimer = null;
|
|
77
|
+
/** Tracks all in-flight fetch Promises so flush() can await them. */
|
|
78
|
+
this.inFlight = /* @__PURE__ */ new Set();
|
|
79
|
+
var _a, _b, _c, _d;
|
|
80
|
+
this.ingestUrl = opts.ingestUrl;
|
|
81
|
+
this.apiKey = opts.apiKey;
|
|
82
|
+
this.timeoutMs = (_a = opts.timeoutMs) != null ? _a : DEFAULT_TIMEOUT_MS;
|
|
83
|
+
this.batchSize = (_b = opts.batchSize) != null ? _b : DEFAULT_BATCH_SIZE;
|
|
84
|
+
this.onError = (_c = opts.onError) != null ? _c : ((err, payloads) => {
|
|
85
|
+
console.warn(
|
|
86
|
+
`[PromptTracer] Failed to deliver ${payloads.length} trace(s):`,
|
|
87
|
+
err.message
|
|
88
|
+
);
|
|
89
|
+
});
|
|
90
|
+
const intervalMs = (_d = opts.flushIntervalMs) != null ? _d : DEFAULT_FLUSH_MS;
|
|
91
|
+
this.flushTimer = setInterval(() => {
|
|
92
|
+
if (this.buffer.length > 0) {
|
|
93
|
+
this._drainBuffer();
|
|
94
|
+
}
|
|
95
|
+
}, intervalMs);
|
|
96
|
+
if (typeof this.flushTimer === "object" && typeof this.flushTimer.unref === "function") {
|
|
97
|
+
this.flushTimer.unref();
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
// ── Public API ─────────────────────────────────────────────────────────────
|
|
101
|
+
/**
|
|
102
|
+
* Accepts a payload and schedules delivery non-blocking via the microtask
|
|
103
|
+
* queue. The caller returns immediately; the POST happens asynchronously.
|
|
104
|
+
*/
|
|
105
|
+
send(payload) {
|
|
106
|
+
Promise.resolve().then(() => {
|
|
107
|
+
this.buffer.push(payload);
|
|
108
|
+
if (this.buffer.length >= this.batchSize) {
|
|
109
|
+
this._drainBuffer();
|
|
110
|
+
}
|
|
111
|
+
});
|
|
112
|
+
}
|
|
113
|
+
/**
|
|
114
|
+
* Waits for all in-flight requests and flushes any remaining buffered
|
|
115
|
+
* payloads. Call this in tests or on process shutdown.
|
|
116
|
+
*
|
|
117
|
+
* @example
|
|
118
|
+
* process.on('SIGTERM', () => tracer.flush());
|
|
119
|
+
*/
|
|
120
|
+
async flush() {
|
|
121
|
+
if (this.buffer.length > 0) {
|
|
122
|
+
this._drainBuffer();
|
|
123
|
+
}
|
|
124
|
+
if (this.inFlight.size > 0) {
|
|
125
|
+
await Promise.allSettled([...this.inFlight]);
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
/**
|
|
129
|
+
* Stops the periodic flush timer and flushes remaining payloads.
|
|
130
|
+
* Call when the Tracer instance is being torn down.
|
|
131
|
+
*/
|
|
132
|
+
async destroy() {
|
|
133
|
+
if (this.flushTimer !== null) {
|
|
134
|
+
clearInterval(this.flushTimer);
|
|
135
|
+
this.flushTimer = null;
|
|
136
|
+
}
|
|
137
|
+
await this.flush();
|
|
138
|
+
}
|
|
139
|
+
// ── Private ────────────────────────────────────────────────────────────────
|
|
140
|
+
/**
|
|
141
|
+
* Atomically snapshots and clears the buffer, then initiates an async
|
|
142
|
+
* POST. Multiple concurrent drains are safe — each works on its own slice.
|
|
143
|
+
*/
|
|
144
|
+
_drainBuffer() {
|
|
145
|
+
const batch = this.buffer.splice(0, this.buffer.length);
|
|
146
|
+
if (batch.length === 0) return;
|
|
147
|
+
const promise = this._sendWithRetry(batch, 1).finally(() => {
|
|
148
|
+
this.inFlight.delete(promise);
|
|
149
|
+
});
|
|
150
|
+
this.inFlight.add(promise);
|
|
151
|
+
}
|
|
152
|
+
/**
|
|
153
|
+
* Attempts to POST a batch to the ingest endpoint.
|
|
154
|
+
* Retries up to MAX_RETRY_ATTEMPTS times with exponential back-off.
|
|
155
|
+
* Only retries on network errors or 5xx responses.
|
|
156
|
+
*/
|
|
157
|
+
async _sendWithRetry(batch, attempt) {
|
|
158
|
+
try {
|
|
159
|
+
await this._post(batch);
|
|
160
|
+
} catch (err) {
|
|
161
|
+
const error = err instanceof Error ? err : new Error(String(err));
|
|
162
|
+
if (attempt < MAX_RETRY_ATTEMPTS) {
|
|
163
|
+
const delay = BASE_RETRY_DELAY_MS * Math.pow(2, attempt - 1);
|
|
164
|
+
await sleep(delay);
|
|
165
|
+
return this._sendWithRetry(batch, attempt + 1);
|
|
166
|
+
}
|
|
167
|
+
this.onError(error, batch);
|
|
168
|
+
}
|
|
169
|
+
}
|
|
170
|
+
/**
|
|
171
|
+
* Performs the raw HTTP POST with an AbortController timeout.
|
|
172
|
+
* Throws on network failure or non-2xx status.
|
|
173
|
+
*/
|
|
174
|
+
async _post(batch) {
|
|
175
|
+
const controller = new AbortController();
|
|
176
|
+
const timer = setTimeout(() => controller.abort(), this.timeoutMs);
|
|
177
|
+
const headers = {
|
|
178
|
+
"Content-Type": "application/json"
|
|
179
|
+
};
|
|
180
|
+
if (this.apiKey) {
|
|
181
|
+
headers["x-api-key"] = this.apiKey;
|
|
182
|
+
}
|
|
183
|
+
let response;
|
|
184
|
+
try {
|
|
185
|
+
response = await fetch(this.ingestUrl, {
|
|
186
|
+
method: "POST",
|
|
187
|
+
headers,
|
|
188
|
+
body: JSON.stringify({ traces: batch }),
|
|
189
|
+
signal: controller.signal
|
|
190
|
+
});
|
|
191
|
+
} finally {
|
|
192
|
+
clearTimeout(timer);
|
|
193
|
+
}
|
|
194
|
+
if (!response.ok) {
|
|
195
|
+
if (response.status >= 500) {
|
|
196
|
+
throw new Error(`Ingest endpoint returned ${response.status}`);
|
|
197
|
+
}
|
|
198
|
+
console.warn(
|
|
199
|
+
`[PromptTracer] Ingest rejected batch (${response.status}). Discarding ${batch.length} trace(s).`
|
|
200
|
+
);
|
|
201
|
+
}
|
|
202
|
+
}
|
|
203
|
+
// ── Helpers ──────────────────────────────────────────────────────────────────
|
|
204
|
+
};
|
|
205
|
+
function sleep(ms) {
|
|
206
|
+
return new Promise((resolve) => setTimeout(resolve, ms));
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
// src/utils/cost.ts
|
|
210
|
+
var MODEL_PRICES = {
|
|
211
|
+
// ── OpenAI ──────────────────────────────────────────────────────────────
|
|
212
|
+
"gpt-4o": { inputPer1M: 2.5, outputPer1M: 10 },
|
|
213
|
+
"gpt-4o-mini": { inputPer1M: 0.15, outputPer1M: 0.6 },
|
|
214
|
+
"gpt-4-turbo": { inputPer1M: 10, outputPer1M: 30 },
|
|
215
|
+
"gpt-4": { inputPer1M: 30, outputPer1M: 60 },
|
|
216
|
+
"gpt-3.5-turbo": { inputPer1M: 0.5, outputPer1M: 1.5 },
|
|
217
|
+
"o1": { inputPer1M: 15, outputPer1M: 60 },
|
|
218
|
+
"o1-mini": { inputPer1M: 3, outputPer1M: 12 },
|
|
219
|
+
"o3-mini": { inputPer1M: 1.1, outputPer1M: 4.4 },
|
|
220
|
+
// ── Anthropic ───────────────────────────────────────────────────────────
|
|
221
|
+
"claude-opus-4": { inputPer1M: 15, outputPer1M: 75 },
|
|
222
|
+
"claude-sonnet-4": { inputPer1M: 3, outputPer1M: 15 },
|
|
223
|
+
"claude-haiku-4": { inputPer1M: 0.8, outputPer1M: 4 },
|
|
224
|
+
"claude-3-5-sonnet": { inputPer1M: 3, outputPer1M: 15 },
|
|
225
|
+
"claude-3-5-haiku": { inputPer1M: 0.8, outputPer1M: 4 },
|
|
226
|
+
"claude-3-opus": { inputPer1M: 15, outputPer1M: 75 },
|
|
227
|
+
// ── Google ──────────────────────────────────────────────────────────────
|
|
228
|
+
"gemini-1.5-pro": { inputPer1M: 3.5, outputPer1M: 10.5 },
|
|
229
|
+
"gemini-1.5-flash": { inputPer1M: 0.35, outputPer1M: 1.05 },
|
|
230
|
+
"gemini-2.0-flash": { inputPer1M: 0.1, outputPer1M: 0.4 }
|
|
231
|
+
};
|
|
232
|
+
var UNKNOWN_PRICE = { inputPer1M: 0, outputPer1M: 0 };
|
|
233
|
+
function resolvePrice(model) {
|
|
234
|
+
const normalised = model.toLowerCase().trim();
|
|
235
|
+
if (normalised in MODEL_PRICES) return MODEL_PRICES[normalised];
|
|
236
|
+
for (const key of Object.keys(MODEL_PRICES)) {
|
|
237
|
+
if (normalised.startsWith(key)) return MODEL_PRICES[key];
|
|
238
|
+
}
|
|
239
|
+
return UNKNOWN_PRICE;
|
|
240
|
+
}
|
|
241
|
+
function calcCostUsd({ model, tokensIn, tokensOut }) {
|
|
242
|
+
const price = resolvePrice(model);
|
|
243
|
+
const inputCost = tokensIn / 1e6 * price.inputPer1M;
|
|
244
|
+
const outputCost = tokensOut / 1e6 * price.outputPer1M;
|
|
245
|
+
return Math.round((inputCost + outputCost) * 1e8) / 1e8;
|
|
246
|
+
}
|
|
247
|
+
function formatCostUsd(usd) {
|
|
248
|
+
if (usd === 0) return "$0.00";
|
|
249
|
+
if (usd < 1e-4) return `$${usd.toFixed(8).replace(/0+$/, "")}`;
|
|
250
|
+
if (usd < 0.01) return `$${usd.toFixed(6).replace(/0+$/, "")}`;
|
|
251
|
+
return `$${usd.toFixed(4)}`;
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
// src/core/tracer.ts
|
|
255
|
+
var SDK_VERSION = "0.1.0";
|
|
256
|
+
function uuid() {
|
|
257
|
+
if (typeof crypto !== "undefined" && typeof crypto.randomUUID === "function") {
|
|
258
|
+
return crypto.randomUUID();
|
|
259
|
+
}
|
|
260
|
+
return "xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx".replace(/[xy]/g, (c) => {
|
|
261
|
+
const r = Math.random() * 16 | 0;
|
|
262
|
+
const v = c === "x" ? r : r & 3 | 8;
|
|
263
|
+
return v.toString(16);
|
|
264
|
+
});
|
|
265
|
+
}
|
|
266
|
+
var Tracer = class {
|
|
267
|
+
constructor(opts, dispatcher) {
|
|
268
|
+
/**
|
|
269
|
+
* Monotonically increasing step counter.
|
|
270
|
+
* Step 1 → first call in the session (triggers full snapshot in the DB).
|
|
271
|
+
* Step N → subsequent calls (store diff only).
|
|
272
|
+
*/
|
|
273
|
+
this.stepCounter = 0;
|
|
274
|
+
var _a, _b, _c;
|
|
275
|
+
this.sessionId = (_a = opts.sessionId) != null ? _a : uuid();
|
|
276
|
+
this.metadata = (_b = opts.metadata) != null ? _b : {};
|
|
277
|
+
this.enabled = (_c = opts.enabled) != null ? _c : true;
|
|
278
|
+
this.dispatcher = dispatcher != null ? dispatcher : new Dispatcher({
|
|
279
|
+
ingestUrl: opts.ingestUrl,
|
|
280
|
+
apiKey: opts.apiKey,
|
|
281
|
+
timeoutMs: opts.timeoutMs,
|
|
282
|
+
onError: opts.onError ? (err, payloads) => payloads.forEach((p) => opts.onError(err, p)) : void 0
|
|
283
|
+
});
|
|
284
|
+
}
|
|
285
|
+
// ── Public API ──────────────────────────────────────────────────────────────
|
|
286
|
+
/**
|
|
287
|
+
* The method called by every SDK wrapper after intercepting an LLM call.
|
|
288
|
+
*
|
|
289
|
+
* Design contract:
|
|
290
|
+
* - NEVER awaited by the wrapper; fire-and-forget on microtask queue.
|
|
291
|
+
* - Returns void so the wrapper cannot accidentally `await` it.
|
|
292
|
+
*
|
|
293
|
+
* @example
|
|
294
|
+
* // Inside wrappers/openai.ts — after receiving the result:
|
|
295
|
+
* tracer.captureAsync({ prompt, response, model, tokensIn, tokensOut, latencyMs, isStream });
|
|
296
|
+
*/
|
|
297
|
+
captureAsync(raw) {
|
|
298
|
+
if (!this.enabled) return;
|
|
299
|
+
Promise.resolve().then(() => {
|
|
300
|
+
try {
|
|
301
|
+
const payload = this._enrich(raw);
|
|
302
|
+
this.dispatcher.send(payload);
|
|
303
|
+
} catch (err) {
|
|
304
|
+
console.warn("[PromptTracer] Failed to enrich payload:", err);
|
|
305
|
+
}
|
|
306
|
+
});
|
|
307
|
+
}
|
|
308
|
+
/**
|
|
309
|
+
* Returns the step index the *next* call will be assigned.
|
|
310
|
+
* Useful for callers who need to know if this is step 1 (full snapshot)
|
|
311
|
+
* vs. a later step (diff only) before making the LLM call.
|
|
312
|
+
*/
|
|
313
|
+
get nextStepIndex() {
|
|
314
|
+
return this.stepCounter + 1;
|
|
315
|
+
}
|
|
316
|
+
/**
|
|
317
|
+
* Waits for all buffered and in-flight payloads to be delivered.
|
|
318
|
+
* Call before process exit or at the end of integration tests.
|
|
319
|
+
*
|
|
320
|
+
* @example
|
|
321
|
+
* afterAll(async () => { await tracer.flush(); });
|
|
322
|
+
*/
|
|
323
|
+
async flush() {
|
|
324
|
+
await this.dispatcher.flush();
|
|
325
|
+
}
|
|
326
|
+
// ── Private ─────────────────────────────────────────────────────────────────
|
|
327
|
+
/**
|
|
328
|
+
* Takes a raw capture from the wrapper and enriches it with:
|
|
329
|
+
* - a unique callId
|
|
330
|
+
* - the session's sessionId
|
|
331
|
+
* - a monotonic stepIndex
|
|
332
|
+
* - ISO-8601 timestamp
|
|
333
|
+
* - USD cost estimate
|
|
334
|
+
* - SDK version string
|
|
335
|
+
* - caller metadata
|
|
336
|
+
*/
|
|
337
|
+
_enrich(raw) {
|
|
338
|
+
var _a, _b;
|
|
339
|
+
this.stepCounter += 1;
|
|
340
|
+
const estimatedCostUsd = calcCostUsd({
|
|
341
|
+
model: raw.model,
|
|
342
|
+
tokensIn: (_a = raw.tokensIn) != null ? _a : 0,
|
|
343
|
+
tokensOut: (_b = raw.tokensOut) != null ? _b : 0
|
|
344
|
+
});
|
|
345
|
+
return __spreadValues(__spreadProps(__spreadValues({
|
|
346
|
+
// ── Core identity ───────────────────────────────────────────────────
|
|
347
|
+
callId: uuid(),
|
|
348
|
+
sessionId: this.sessionId,
|
|
349
|
+
stepIndex: this.stepCounter,
|
|
350
|
+
timestamp: (/* @__PURE__ */ new Date()).toISOString()
|
|
351
|
+
}, raw), {
|
|
352
|
+
// ── Enrichment ───────────────────────────────────────────────────────
|
|
353
|
+
estimatedCostUsd,
|
|
354
|
+
sdkVersion: SDK_VERSION
|
|
355
|
+
}), Object.keys(this.metadata).length > 0 ? { metadata: this.metadata } : {});
|
|
356
|
+
}
|
|
357
|
+
};
|
|
358
|
+
|
|
359
|
+
// src/wrappers/openai.ts
|
|
360
|
+
function wrapOpenAI(client, tracer) {
|
|
361
|
+
return new Proxy(client, {
|
|
362
|
+
get(target, prop, receiver) {
|
|
363
|
+
if (prop === "chat") {
|
|
364
|
+
return new Proxy(target.chat, {
|
|
365
|
+
get(chatTarget, chatProp, chatReceiver) {
|
|
366
|
+
if (chatProp === "completions") {
|
|
367
|
+
return new Proxy(chatTarget.completions, {
|
|
368
|
+
get(compTarget, compProp, compReceiver) {
|
|
369
|
+
if (compProp === "create") {
|
|
370
|
+
return _makeCreateInterceptor(compTarget, tracer);
|
|
371
|
+
}
|
|
372
|
+
return Reflect.get(compTarget, compProp, compReceiver);
|
|
373
|
+
}
|
|
374
|
+
});
|
|
375
|
+
}
|
|
376
|
+
return Reflect.get(chatTarget, chatProp, chatReceiver);
|
|
377
|
+
}
|
|
378
|
+
});
|
|
379
|
+
}
|
|
380
|
+
return Reflect.get(target, prop, receiver);
|
|
381
|
+
}
|
|
382
|
+
});
|
|
383
|
+
}
|
|
384
|
+
function _makeCreateInterceptor(compTarget, tracer) {
|
|
385
|
+
async function create(params) {
|
|
386
|
+
var _a, _b, _c, _d, _e;
|
|
387
|
+
const startMs = Date.now();
|
|
388
|
+
if (params.stream === true) {
|
|
389
|
+
const stream = await compTarget.create(params);
|
|
390
|
+
return _wrapStream(stream, params, startMs, tracer);
|
|
391
|
+
}
|
|
392
|
+
const result = await compTarget.create(params);
|
|
393
|
+
const latencyMs = Date.now() - startMs;
|
|
394
|
+
tracer.captureAsync({
|
|
395
|
+
prompt: params.messages,
|
|
396
|
+
response: (_c = (_b = (_a = result.choices[0]) == null ? void 0 : _a.message) == null ? void 0 : _b.content) != null ? _c : "",
|
|
397
|
+
model: params.model,
|
|
398
|
+
tokensIn: (_d = result.usage) == null ? void 0 : _d.prompt_tokens,
|
|
399
|
+
tokensOut: (_e = result.usage) == null ? void 0 : _e.completion_tokens,
|
|
400
|
+
latencyMs,
|
|
401
|
+
isStream: false
|
|
402
|
+
});
|
|
403
|
+
return result;
|
|
404
|
+
}
|
|
405
|
+
return create;
|
|
406
|
+
}
|
|
407
|
+
function _wrapStream(stream, params, startMs, tracer) {
|
|
408
|
+
return __asyncGenerator(this, null, function* () {
|
|
409
|
+
var _a, _b, _c, _d;
|
|
410
|
+
let fullContent = "";
|
|
411
|
+
let chunkCount = 0;
|
|
412
|
+
let promptTokens;
|
|
413
|
+
try {
|
|
414
|
+
try {
|
|
415
|
+
for (var iter = __forAwait(stream), more, temp, error; more = !(temp = yield new __await(iter.next())).done; more = false) {
|
|
416
|
+
const chunk = temp.value;
|
|
417
|
+
if (((_a = chunk.usage) == null ? void 0 : _a.prompt_tokens) !== void 0) {
|
|
418
|
+
promptTokens = chunk.usage.prompt_tokens;
|
|
419
|
+
}
|
|
420
|
+
const delta = (_d = (_c = (_b = chunk.choices[0]) == null ? void 0 : _b.delta) == null ? void 0 : _c.content) != null ? _d : "";
|
|
421
|
+
fullContent += delta;
|
|
422
|
+
chunkCount += 1;
|
|
423
|
+
yield chunk;
|
|
424
|
+
}
|
|
425
|
+
} catch (temp) {
|
|
426
|
+
error = [temp];
|
|
427
|
+
} finally {
|
|
428
|
+
try {
|
|
429
|
+
more && (temp = iter.return) && (yield new __await(temp.call(iter)));
|
|
430
|
+
} finally {
|
|
431
|
+
if (error)
|
|
432
|
+
throw error[0];
|
|
433
|
+
}
|
|
434
|
+
}
|
|
435
|
+
} finally {
|
|
436
|
+
const latencyMs = Date.now() - startMs;
|
|
437
|
+
tracer.captureAsync({
|
|
438
|
+
prompt: params.messages,
|
|
439
|
+
response: fullContent,
|
|
440
|
+
model: params.model,
|
|
441
|
+
tokensIn: promptTokens,
|
|
442
|
+
// exact if include_usage was set
|
|
443
|
+
tokensOut: chunkCount,
|
|
444
|
+
// approximation: 1 chunk ≈ 1 token
|
|
445
|
+
latencyMs,
|
|
446
|
+
isStream: true
|
|
447
|
+
});
|
|
448
|
+
}
|
|
449
|
+
});
|
|
450
|
+
}
|
|
451
|
+
// Annotate the CommonJS export names for ESM import in node:
|
|
452
|
+
0 && (module.exports = {
|
|
453
|
+
Dispatcher,
|
|
454
|
+
Tracer,
|
|
455
|
+
calcCostUsd,
|
|
456
|
+
formatCostUsd,
|
|
457
|
+
wrapOpenAI
|
|
458
|
+
});
|
package/dist/index.js
CHANGED
|
@@ -1 +1,430 @@
|
|
|
1
|
-
|
|
1
|
+
var __defProp = Object.defineProperty;
|
|
2
|
+
var __defProps = Object.defineProperties;
|
|
3
|
+
var __getOwnPropDescs = Object.getOwnPropertyDescriptors;
|
|
4
|
+
var __getOwnPropSymbols = Object.getOwnPropertySymbols;
|
|
5
|
+
var __hasOwnProp = Object.prototype.hasOwnProperty;
|
|
6
|
+
var __propIsEnum = Object.prototype.propertyIsEnumerable;
|
|
7
|
+
var __knownSymbol = (name, symbol) => (symbol = Symbol[name]) ? symbol : /* @__PURE__ */ Symbol.for("Symbol." + name);
|
|
8
|
+
var __defNormalProp = (obj, key, value) => key in obj ? __defProp(obj, key, { enumerable: true, configurable: true, writable: true, value }) : obj[key] = value;
|
|
9
|
+
var __spreadValues = (a, b) => {
|
|
10
|
+
for (var prop in b || (b = {}))
|
|
11
|
+
if (__hasOwnProp.call(b, prop))
|
|
12
|
+
__defNormalProp(a, prop, b[prop]);
|
|
13
|
+
if (__getOwnPropSymbols)
|
|
14
|
+
for (var prop of __getOwnPropSymbols(b)) {
|
|
15
|
+
if (__propIsEnum.call(b, prop))
|
|
16
|
+
__defNormalProp(a, prop, b[prop]);
|
|
17
|
+
}
|
|
18
|
+
return a;
|
|
19
|
+
};
|
|
20
|
+
var __spreadProps = (a, b) => __defProps(a, __getOwnPropDescs(b));
|
|
21
|
+
var __await = function(promise, isYieldStar) {
|
|
22
|
+
this[0] = promise;
|
|
23
|
+
this[1] = isYieldStar;
|
|
24
|
+
};
|
|
25
|
+
var __asyncGenerator = (__this, __arguments, generator) => {
|
|
26
|
+
var resume = (k, v, yes, no) => {
|
|
27
|
+
try {
|
|
28
|
+
var x = generator[k](v), isAwait = (v = x.value) instanceof __await, done = x.done;
|
|
29
|
+
Promise.resolve(isAwait ? v[0] : v).then((y) => isAwait ? resume(k === "return" ? k : "next", v[1] ? { done: y.done, value: y.value } : y, yes, no) : yes({ value: y, done })).catch((e) => resume("throw", e, yes, no));
|
|
30
|
+
} catch (e) {
|
|
31
|
+
no(e);
|
|
32
|
+
}
|
|
33
|
+
}, method = (k, call, wait, clear) => it[k] = (x) => (call = new Promise((yes, no, run) => (run = () => resume(k, x, yes, no), q ? q.then(run) : run())), clear = () => q === wait && (q = 0), q = wait = call.then(clear, clear), call), q, it = {};
|
|
34
|
+
return generator = generator.apply(__this, __arguments), it[__knownSymbol("asyncIterator")] = () => it, method("next"), method("throw"), method("return"), it;
|
|
35
|
+
};
|
|
36
|
+
var __forAwait = (obj, it, method) => (it = obj[__knownSymbol("asyncIterator")]) ? it.call(obj) : (obj = obj[__knownSymbol("iterator")](), it = {}, method = (key, fn) => (fn = obj[key]) && (it[key] = (arg) => new Promise((yes, no, done) => (arg = fn.call(obj, arg), done = arg.done, Promise.resolve(arg.value).then((value) => yes({ value, done }), no)))), method("next"), method("return"), it);
|
|
37
|
+
|
|
38
|
+
// src/core/dispatcher.ts
|
|
39
|
+
var DEFAULT_BATCH_SIZE = 10;
|
|
40
|
+
var DEFAULT_FLUSH_MS = 2e3;
|
|
41
|
+
var DEFAULT_TIMEOUT_MS = 5e3;
|
|
42
|
+
var MAX_RETRY_ATTEMPTS = 3;
|
|
43
|
+
var BASE_RETRY_DELAY_MS = 200;
|
|
44
|
+
var Dispatcher = class {
|
|
45
|
+
constructor(opts) {
|
|
46
|
+
/** The in-memory buffer accumulating payloads between flushes. */
|
|
47
|
+
this.buffer = [];
|
|
48
|
+
/** The NodeJS/browser timer handle for the periodic flush. */
|
|
49
|
+
this.flushTimer = null;
|
|
50
|
+
/** Tracks all in-flight fetch Promises so flush() can await them. */
|
|
51
|
+
this.inFlight = /* @__PURE__ */ new Set();
|
|
52
|
+
var _a, _b, _c, _d;
|
|
53
|
+
this.ingestUrl = opts.ingestUrl;
|
|
54
|
+
this.apiKey = opts.apiKey;
|
|
55
|
+
this.timeoutMs = (_a = opts.timeoutMs) != null ? _a : DEFAULT_TIMEOUT_MS;
|
|
56
|
+
this.batchSize = (_b = opts.batchSize) != null ? _b : DEFAULT_BATCH_SIZE;
|
|
57
|
+
this.onError = (_c = opts.onError) != null ? _c : ((err, payloads) => {
|
|
58
|
+
console.warn(
|
|
59
|
+
`[PromptTracer] Failed to deliver ${payloads.length} trace(s):`,
|
|
60
|
+
err.message
|
|
61
|
+
);
|
|
62
|
+
});
|
|
63
|
+
const intervalMs = (_d = opts.flushIntervalMs) != null ? _d : DEFAULT_FLUSH_MS;
|
|
64
|
+
this.flushTimer = setInterval(() => {
|
|
65
|
+
if (this.buffer.length > 0) {
|
|
66
|
+
this._drainBuffer();
|
|
67
|
+
}
|
|
68
|
+
}, intervalMs);
|
|
69
|
+
if (typeof this.flushTimer === "object" && typeof this.flushTimer.unref === "function") {
|
|
70
|
+
this.flushTimer.unref();
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
// ── Public API ─────────────────────────────────────────────────────────────
|
|
74
|
+
/**
|
|
75
|
+
* Accepts a payload and schedules delivery non-blocking via the microtask
|
|
76
|
+
* queue. The caller returns immediately; the POST happens asynchronously.
|
|
77
|
+
*/
|
|
78
|
+
send(payload) {
|
|
79
|
+
Promise.resolve().then(() => {
|
|
80
|
+
this.buffer.push(payload);
|
|
81
|
+
if (this.buffer.length >= this.batchSize) {
|
|
82
|
+
this._drainBuffer();
|
|
83
|
+
}
|
|
84
|
+
});
|
|
85
|
+
}
|
|
86
|
+
/**
|
|
87
|
+
* Waits for all in-flight requests and flushes any remaining buffered
|
|
88
|
+
* payloads. Call this in tests or on process shutdown.
|
|
89
|
+
*
|
|
90
|
+
* @example
|
|
91
|
+
* process.on('SIGTERM', () => tracer.flush());
|
|
92
|
+
*/
|
|
93
|
+
async flush() {
|
|
94
|
+
if (this.buffer.length > 0) {
|
|
95
|
+
this._drainBuffer();
|
|
96
|
+
}
|
|
97
|
+
if (this.inFlight.size > 0) {
|
|
98
|
+
await Promise.allSettled([...this.inFlight]);
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
/**
|
|
102
|
+
* Stops the periodic flush timer and flushes remaining payloads.
|
|
103
|
+
* Call when the Tracer instance is being torn down.
|
|
104
|
+
*/
|
|
105
|
+
async destroy() {
|
|
106
|
+
if (this.flushTimer !== null) {
|
|
107
|
+
clearInterval(this.flushTimer);
|
|
108
|
+
this.flushTimer = null;
|
|
109
|
+
}
|
|
110
|
+
await this.flush();
|
|
111
|
+
}
|
|
112
|
+
// ── Private ────────────────────────────────────────────────────────────────
|
|
113
|
+
/**
|
|
114
|
+
* Atomically snapshots and clears the buffer, then initiates an async
|
|
115
|
+
* POST. Multiple concurrent drains are safe — each works on its own slice.
|
|
116
|
+
*/
|
|
117
|
+
_drainBuffer() {
|
|
118
|
+
const batch = this.buffer.splice(0, this.buffer.length);
|
|
119
|
+
if (batch.length === 0) return;
|
|
120
|
+
const promise = this._sendWithRetry(batch, 1).finally(() => {
|
|
121
|
+
this.inFlight.delete(promise);
|
|
122
|
+
});
|
|
123
|
+
this.inFlight.add(promise);
|
|
124
|
+
}
|
|
125
|
+
/**
|
|
126
|
+
* Attempts to POST a batch to the ingest endpoint.
|
|
127
|
+
* Retries up to MAX_RETRY_ATTEMPTS times with exponential back-off.
|
|
128
|
+
* Only retries on network errors or 5xx responses.
|
|
129
|
+
*/
|
|
130
|
+
async _sendWithRetry(batch, attempt) {
|
|
131
|
+
try {
|
|
132
|
+
await this._post(batch);
|
|
133
|
+
} catch (err) {
|
|
134
|
+
const error = err instanceof Error ? err : new Error(String(err));
|
|
135
|
+
if (attempt < MAX_RETRY_ATTEMPTS) {
|
|
136
|
+
const delay = BASE_RETRY_DELAY_MS * Math.pow(2, attempt - 1);
|
|
137
|
+
await sleep(delay);
|
|
138
|
+
return this._sendWithRetry(batch, attempt + 1);
|
|
139
|
+
}
|
|
140
|
+
this.onError(error, batch);
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
/**
|
|
144
|
+
* Performs the raw HTTP POST with an AbortController timeout.
|
|
145
|
+
* Throws on network failure or non-2xx status.
|
|
146
|
+
*/
|
|
147
|
+
async _post(batch) {
|
|
148
|
+
const controller = new AbortController();
|
|
149
|
+
const timer = setTimeout(() => controller.abort(), this.timeoutMs);
|
|
150
|
+
const headers = {
|
|
151
|
+
"Content-Type": "application/json"
|
|
152
|
+
};
|
|
153
|
+
if (this.apiKey) {
|
|
154
|
+
headers["x-api-key"] = this.apiKey;
|
|
155
|
+
}
|
|
156
|
+
let response;
|
|
157
|
+
try {
|
|
158
|
+
response = await fetch(this.ingestUrl, {
|
|
159
|
+
method: "POST",
|
|
160
|
+
headers,
|
|
161
|
+
body: JSON.stringify({ traces: batch }),
|
|
162
|
+
signal: controller.signal
|
|
163
|
+
});
|
|
164
|
+
} finally {
|
|
165
|
+
clearTimeout(timer);
|
|
166
|
+
}
|
|
167
|
+
if (!response.ok) {
|
|
168
|
+
if (response.status >= 500) {
|
|
169
|
+
throw new Error(`Ingest endpoint returned ${response.status}`);
|
|
170
|
+
}
|
|
171
|
+
console.warn(
|
|
172
|
+
`[PromptTracer] Ingest rejected batch (${response.status}). Discarding ${batch.length} trace(s).`
|
|
173
|
+
);
|
|
174
|
+
}
|
|
175
|
+
}
|
|
176
|
+
// ── Helpers ──────────────────────────────────────────────────────────────────
|
|
177
|
+
};
|
|
178
|
+
function sleep(ms) {
|
|
179
|
+
return new Promise((resolve) => setTimeout(resolve, ms));
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
// src/utils/cost.ts
|
|
183
|
+
var MODEL_PRICES = {
|
|
184
|
+
// ── OpenAI ──────────────────────────────────────────────────────────────
|
|
185
|
+
"gpt-4o": { inputPer1M: 2.5, outputPer1M: 10 },
|
|
186
|
+
"gpt-4o-mini": { inputPer1M: 0.15, outputPer1M: 0.6 },
|
|
187
|
+
"gpt-4-turbo": { inputPer1M: 10, outputPer1M: 30 },
|
|
188
|
+
"gpt-4": { inputPer1M: 30, outputPer1M: 60 },
|
|
189
|
+
"gpt-3.5-turbo": { inputPer1M: 0.5, outputPer1M: 1.5 },
|
|
190
|
+
"o1": { inputPer1M: 15, outputPer1M: 60 },
|
|
191
|
+
"o1-mini": { inputPer1M: 3, outputPer1M: 12 },
|
|
192
|
+
"o3-mini": { inputPer1M: 1.1, outputPer1M: 4.4 },
|
|
193
|
+
// ── Anthropic ───────────────────────────────────────────────────────────
|
|
194
|
+
"claude-opus-4": { inputPer1M: 15, outputPer1M: 75 },
|
|
195
|
+
"claude-sonnet-4": { inputPer1M: 3, outputPer1M: 15 },
|
|
196
|
+
"claude-haiku-4": { inputPer1M: 0.8, outputPer1M: 4 },
|
|
197
|
+
"claude-3-5-sonnet": { inputPer1M: 3, outputPer1M: 15 },
|
|
198
|
+
"claude-3-5-haiku": { inputPer1M: 0.8, outputPer1M: 4 },
|
|
199
|
+
"claude-3-opus": { inputPer1M: 15, outputPer1M: 75 },
|
|
200
|
+
// ── Google ──────────────────────────────────────────────────────────────
|
|
201
|
+
"gemini-1.5-pro": { inputPer1M: 3.5, outputPer1M: 10.5 },
|
|
202
|
+
"gemini-1.5-flash": { inputPer1M: 0.35, outputPer1M: 1.05 },
|
|
203
|
+
"gemini-2.0-flash": { inputPer1M: 0.1, outputPer1M: 0.4 }
|
|
204
|
+
};
|
|
205
|
+
var UNKNOWN_PRICE = { inputPer1M: 0, outputPer1M: 0 };
|
|
206
|
+
function resolvePrice(model) {
|
|
207
|
+
const normalised = model.toLowerCase().trim();
|
|
208
|
+
if (normalised in MODEL_PRICES) return MODEL_PRICES[normalised];
|
|
209
|
+
for (const key of Object.keys(MODEL_PRICES)) {
|
|
210
|
+
if (normalised.startsWith(key)) return MODEL_PRICES[key];
|
|
211
|
+
}
|
|
212
|
+
return UNKNOWN_PRICE;
|
|
213
|
+
}
|
|
214
|
+
function calcCostUsd({ model, tokensIn, tokensOut }) {
|
|
215
|
+
const price = resolvePrice(model);
|
|
216
|
+
const inputCost = tokensIn / 1e6 * price.inputPer1M;
|
|
217
|
+
const outputCost = tokensOut / 1e6 * price.outputPer1M;
|
|
218
|
+
return Math.round((inputCost + outputCost) * 1e8) / 1e8;
|
|
219
|
+
}
|
|
220
|
+
function formatCostUsd(usd) {
|
|
221
|
+
if (usd === 0) return "$0.00";
|
|
222
|
+
if (usd < 1e-4) return `$${usd.toFixed(8).replace(/0+$/, "")}`;
|
|
223
|
+
if (usd < 0.01) return `$${usd.toFixed(6).replace(/0+$/, "")}`;
|
|
224
|
+
return `$${usd.toFixed(4)}`;
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
// src/core/tracer.ts
|
|
228
|
+
var SDK_VERSION = "0.1.0";
|
|
229
|
+
function uuid() {
|
|
230
|
+
if (typeof crypto !== "undefined" && typeof crypto.randomUUID === "function") {
|
|
231
|
+
return crypto.randomUUID();
|
|
232
|
+
}
|
|
233
|
+
return "xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx".replace(/[xy]/g, (c) => {
|
|
234
|
+
const r = Math.random() * 16 | 0;
|
|
235
|
+
const v = c === "x" ? r : r & 3 | 8;
|
|
236
|
+
return v.toString(16);
|
|
237
|
+
});
|
|
238
|
+
}
|
|
239
|
+
var Tracer = class {
|
|
240
|
+
constructor(opts, dispatcher) {
|
|
241
|
+
/**
|
|
242
|
+
* Monotonically increasing step counter.
|
|
243
|
+
* Step 1 → first call in the session (triggers full snapshot in the DB).
|
|
244
|
+
* Step N → subsequent calls (store diff only).
|
|
245
|
+
*/
|
|
246
|
+
this.stepCounter = 0;
|
|
247
|
+
var _a, _b, _c;
|
|
248
|
+
this.sessionId = (_a = opts.sessionId) != null ? _a : uuid();
|
|
249
|
+
this.metadata = (_b = opts.metadata) != null ? _b : {};
|
|
250
|
+
this.enabled = (_c = opts.enabled) != null ? _c : true;
|
|
251
|
+
this.dispatcher = dispatcher != null ? dispatcher : new Dispatcher({
|
|
252
|
+
ingestUrl: opts.ingestUrl,
|
|
253
|
+
apiKey: opts.apiKey,
|
|
254
|
+
timeoutMs: opts.timeoutMs,
|
|
255
|
+
onError: opts.onError ? (err, payloads) => payloads.forEach((p) => opts.onError(err, p)) : void 0
|
|
256
|
+
});
|
|
257
|
+
}
|
|
258
|
+
// ── Public API ──────────────────────────────────────────────────────────────
|
|
259
|
+
/**
|
|
260
|
+
* The method called by every SDK wrapper after intercepting an LLM call.
|
|
261
|
+
*
|
|
262
|
+
* Design contract:
|
|
263
|
+
* - NEVER awaited by the wrapper; fire-and-forget on microtask queue.
|
|
264
|
+
* - Returns void so the wrapper cannot accidentally `await` it.
|
|
265
|
+
*
|
|
266
|
+
* @example
|
|
267
|
+
* // Inside wrappers/openai.ts — after receiving the result:
|
|
268
|
+
* tracer.captureAsync({ prompt, response, model, tokensIn, tokensOut, latencyMs, isStream });
|
|
269
|
+
*/
|
|
270
|
+
captureAsync(raw) {
|
|
271
|
+
if (!this.enabled) return;
|
|
272
|
+
Promise.resolve().then(() => {
|
|
273
|
+
try {
|
|
274
|
+
const payload = this._enrich(raw);
|
|
275
|
+
this.dispatcher.send(payload);
|
|
276
|
+
} catch (err) {
|
|
277
|
+
console.warn("[PromptTracer] Failed to enrich payload:", err);
|
|
278
|
+
}
|
|
279
|
+
});
|
|
280
|
+
}
|
|
281
|
+
/**
|
|
282
|
+
* Returns the step index the *next* call will be assigned.
|
|
283
|
+
* Useful for callers who need to know if this is step 1 (full snapshot)
|
|
284
|
+
* vs. a later step (diff only) before making the LLM call.
|
|
285
|
+
*/
|
|
286
|
+
get nextStepIndex() {
|
|
287
|
+
return this.stepCounter + 1;
|
|
288
|
+
}
|
|
289
|
+
/**
|
|
290
|
+
* Waits for all buffered and in-flight payloads to be delivered.
|
|
291
|
+
* Call before process exit or at the end of integration tests.
|
|
292
|
+
*
|
|
293
|
+
* @example
|
|
294
|
+
* afterAll(async () => { await tracer.flush(); });
|
|
295
|
+
*/
|
|
296
|
+
async flush() {
|
|
297
|
+
await this.dispatcher.flush();
|
|
298
|
+
}
|
|
299
|
+
// ── Private ─────────────────────────────────────────────────────────────────
|
|
300
|
+
/**
|
|
301
|
+
* Takes a raw capture from the wrapper and enriches it with:
|
|
302
|
+
* - a unique callId
|
|
303
|
+
* - the session's sessionId
|
|
304
|
+
* - a monotonic stepIndex
|
|
305
|
+
* - ISO-8601 timestamp
|
|
306
|
+
* - USD cost estimate
|
|
307
|
+
* - SDK version string
|
|
308
|
+
* - caller metadata
|
|
309
|
+
*/
|
|
310
|
+
_enrich(raw) {
|
|
311
|
+
var _a, _b;
|
|
312
|
+
this.stepCounter += 1;
|
|
313
|
+
const estimatedCostUsd = calcCostUsd({
|
|
314
|
+
model: raw.model,
|
|
315
|
+
tokensIn: (_a = raw.tokensIn) != null ? _a : 0,
|
|
316
|
+
tokensOut: (_b = raw.tokensOut) != null ? _b : 0
|
|
317
|
+
});
|
|
318
|
+
return __spreadValues(__spreadProps(__spreadValues({
|
|
319
|
+
// ── Core identity ───────────────────────────────────────────────────
|
|
320
|
+
callId: uuid(),
|
|
321
|
+
sessionId: this.sessionId,
|
|
322
|
+
stepIndex: this.stepCounter,
|
|
323
|
+
timestamp: (/* @__PURE__ */ new Date()).toISOString()
|
|
324
|
+
}, raw), {
|
|
325
|
+
// ── Enrichment ───────────────────────────────────────────────────────
|
|
326
|
+
estimatedCostUsd,
|
|
327
|
+
sdkVersion: SDK_VERSION
|
|
328
|
+
}), Object.keys(this.metadata).length > 0 ? { metadata: this.metadata } : {});
|
|
329
|
+
}
|
|
330
|
+
};
|
|
331
|
+
|
|
332
|
+
// src/wrappers/openai.ts
|
|
333
|
+
function wrapOpenAI(client, tracer) {
|
|
334
|
+
return new Proxy(client, {
|
|
335
|
+
get(target, prop, receiver) {
|
|
336
|
+
if (prop === "chat") {
|
|
337
|
+
return new Proxy(target.chat, {
|
|
338
|
+
get(chatTarget, chatProp, chatReceiver) {
|
|
339
|
+
if (chatProp === "completions") {
|
|
340
|
+
return new Proxy(chatTarget.completions, {
|
|
341
|
+
get(compTarget, compProp, compReceiver) {
|
|
342
|
+
if (compProp === "create") {
|
|
343
|
+
return _makeCreateInterceptor(compTarget, tracer);
|
|
344
|
+
}
|
|
345
|
+
return Reflect.get(compTarget, compProp, compReceiver);
|
|
346
|
+
}
|
|
347
|
+
});
|
|
348
|
+
}
|
|
349
|
+
return Reflect.get(chatTarget, chatProp, chatReceiver);
|
|
350
|
+
}
|
|
351
|
+
});
|
|
352
|
+
}
|
|
353
|
+
return Reflect.get(target, prop, receiver);
|
|
354
|
+
}
|
|
355
|
+
});
|
|
356
|
+
}
|
|
357
|
+
function _makeCreateInterceptor(compTarget, tracer) {
|
|
358
|
+
async function create(params) {
|
|
359
|
+
var _a, _b, _c, _d, _e;
|
|
360
|
+
const startMs = Date.now();
|
|
361
|
+
if (params.stream === true) {
|
|
362
|
+
const stream = await compTarget.create(params);
|
|
363
|
+
return _wrapStream(stream, params, startMs, tracer);
|
|
364
|
+
}
|
|
365
|
+
const result = await compTarget.create(params);
|
|
366
|
+
const latencyMs = Date.now() - startMs;
|
|
367
|
+
tracer.captureAsync({
|
|
368
|
+
prompt: params.messages,
|
|
369
|
+
response: (_c = (_b = (_a = result.choices[0]) == null ? void 0 : _a.message) == null ? void 0 : _b.content) != null ? _c : "",
|
|
370
|
+
model: params.model,
|
|
371
|
+
tokensIn: (_d = result.usage) == null ? void 0 : _d.prompt_tokens,
|
|
372
|
+
tokensOut: (_e = result.usage) == null ? void 0 : _e.completion_tokens,
|
|
373
|
+
latencyMs,
|
|
374
|
+
isStream: false
|
|
375
|
+
});
|
|
376
|
+
return result;
|
|
377
|
+
}
|
|
378
|
+
return create;
|
|
379
|
+
}
|
|
380
|
+
function _wrapStream(stream, params, startMs, tracer) {
|
|
381
|
+
return __asyncGenerator(this, null, function* () {
|
|
382
|
+
var _a, _b, _c, _d;
|
|
383
|
+
let fullContent = "";
|
|
384
|
+
let chunkCount = 0;
|
|
385
|
+
let promptTokens;
|
|
386
|
+
try {
|
|
387
|
+
try {
|
|
388
|
+
for (var iter = __forAwait(stream), more, temp, error; more = !(temp = yield new __await(iter.next())).done; more = false) {
|
|
389
|
+
const chunk = temp.value;
|
|
390
|
+
if (((_a = chunk.usage) == null ? void 0 : _a.prompt_tokens) !== void 0) {
|
|
391
|
+
promptTokens = chunk.usage.prompt_tokens;
|
|
392
|
+
}
|
|
393
|
+
const delta = (_d = (_c = (_b = chunk.choices[0]) == null ? void 0 : _b.delta) == null ? void 0 : _c.content) != null ? _d : "";
|
|
394
|
+
fullContent += delta;
|
|
395
|
+
chunkCount += 1;
|
|
396
|
+
yield chunk;
|
|
397
|
+
}
|
|
398
|
+
} catch (temp) {
|
|
399
|
+
error = [temp];
|
|
400
|
+
} finally {
|
|
401
|
+
try {
|
|
402
|
+
more && (temp = iter.return) && (yield new __await(temp.call(iter)));
|
|
403
|
+
} finally {
|
|
404
|
+
if (error)
|
|
405
|
+
throw error[0];
|
|
406
|
+
}
|
|
407
|
+
}
|
|
408
|
+
} finally {
|
|
409
|
+
const latencyMs = Date.now() - startMs;
|
|
410
|
+
tracer.captureAsync({
|
|
411
|
+
prompt: params.messages,
|
|
412
|
+
response: fullContent,
|
|
413
|
+
model: params.model,
|
|
414
|
+
tokensIn: promptTokens,
|
|
415
|
+
// exact if include_usage was set
|
|
416
|
+
tokensOut: chunkCount,
|
|
417
|
+
// approximation: 1 chunk ≈ 1 token
|
|
418
|
+
latencyMs,
|
|
419
|
+
isStream: true
|
|
420
|
+
});
|
|
421
|
+
}
|
|
422
|
+
});
|
|
423
|
+
}
|
|
424
|
+
export {
|
|
425
|
+
Dispatcher,
|
|
426
|
+
Tracer,
|
|
427
|
+
calcCostUsd,
|
|
428
|
+
formatCostUsd,
|
|
429
|
+
wrapOpenAI
|
|
430
|
+
};
|
package/package.json
CHANGED
|
@@ -1,19 +1,28 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "0xtrace",
|
|
3
|
-
"version": "1.0.
|
|
4
|
-
"description": "",
|
|
5
|
-
"
|
|
6
|
-
"
|
|
7
|
-
"
|
|
3
|
+
"version": "1.0.2",
|
|
4
|
+
"description": "Proxy-based LLM telemetry SDK for 0xtrace",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"main": "./dist/index.cjs",
|
|
7
|
+
"module": "./dist/index.js",
|
|
8
|
+
"exports": {
|
|
9
|
+
".": {
|
|
10
|
+
"import": "./dist/index.js",
|
|
11
|
+
"require": "./dist/index.cjs"
|
|
12
|
+
}
|
|
13
|
+
},
|
|
14
|
+
"files": ["dist"],
|
|
8
15
|
"scripts": {
|
|
9
|
-
"build": "tsup"
|
|
10
|
-
|
|
16
|
+
"build": "tsup"
|
|
17
|
+
},
|
|
18
|
+
"dependencies": {
|
|
19
|
+
"diff": "^9.0.0",
|
|
20
|
+
"openai": "^6.38.0"
|
|
11
21
|
},
|
|
12
|
-
"author": "Sidhant Kumar",
|
|
13
|
-
"license": "MIT",
|
|
14
22
|
"devDependencies": {
|
|
15
|
-
"@types/node": "^25.9.
|
|
23
|
+
"@types/node": "^25.9.0",
|
|
24
|
+
"cac": "^6.7.14",
|
|
16
25
|
"tsup": "^8.5.1",
|
|
17
26
|
"typescript": "^6.0.3"
|
|
18
27
|
}
|
|
19
|
-
}
|
|
28
|
+
}
|
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/src/index.ts
DELETED
|
@@ -1,37 +0,0 @@
|
|
|
1
|
-
export 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
|
-
|
|
13
|
-
|
|
14
|
-
// Import process for Node.js type support
|
|
15
|
-
|
|
16
|
-
export async function traceLlmCall(payload: TracePayload) {
|
|
17
|
-
const redisUrl = process.env.UPSTASH_REDIS_REST_URL;
|
|
18
|
-
const redisToken = process.env.UPSTASH_REDIS_REST_TOKEN;
|
|
19
|
-
|
|
20
|
-
if (!redisUrl || !redisToken) return;
|
|
21
|
-
|
|
22
|
-
try {
|
|
23
|
-
fetch(`${redisUrl}/lpush/0xtrace_queue`, {
|
|
24
|
-
method: "POST",
|
|
25
|
-
headers: {
|
|
26
|
-
Authorization: `Bearer ${redisToken}`,
|
|
27
|
-
"Content-Type": "application/json",
|
|
28
|
-
},
|
|
29
|
-
body: JSON.stringify({
|
|
30
|
-
...payload,
|
|
31
|
-
timestamp: new Date().toISOString(),
|
|
32
|
-
}),
|
|
33
|
-
}).catch((e) => console.error(e));
|
|
34
|
-
} catch (error) {
|
|
35
|
-
console.error(error);
|
|
36
|
-
}
|
|
37
|
-
}
|
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
|
-
}
|
package/tsup.config.ts
DELETED
|
@@ -1,12 +0,0 @@
|
|
|
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,
|
|
10
|
-
clean: true,
|
|
11
|
-
minify: true
|
|
12
|
-
});
|