@reaatech/llm-cost-telemetry-providers 0.1.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.
package/dist/index.js ADDED
@@ -0,0 +1,434 @@
1
+ // src/base.ts
2
+ import { generateId } from "@reaatech/llm-cost-telemetry";
3
+ var BaseProviderWrapper = class {
4
+ /** The wrapped client */
5
+ client;
6
+ /** Callback for cost spans */
7
+ onSpanCallback = null;
8
+ /** Default telemetry context */
9
+ defaultContext = {};
10
+ /**
11
+ * Create a new provider wrapper
12
+ */
13
+ constructor(client) {
14
+ this.client = client;
15
+ }
16
+ /**
17
+ * Set the callback for cost spans
18
+ */
19
+ onSpan(callback) {
20
+ this.onSpanCallback = callback;
21
+ }
22
+ /**
23
+ * Set default telemetry context
24
+ */
25
+ setDefaultContext(context) {
26
+ this.defaultContext = context;
27
+ }
28
+ /**
29
+ * Create a cost span from request and response metadata
30
+ */
31
+ createSpan(request, response) {
32
+ const duration = response.endTime.getTime() - request.startTime.getTime();
33
+ return {
34
+ id: generateId(),
35
+ provider: this.provider,
36
+ model: request.model,
37
+ inputTokens: response.inputTokens,
38
+ outputTokens: response.outputTokens,
39
+ totalTokens: response.inputTokens + response.outputTokens,
40
+ costUsd: 0,
41
+ // Will be calculated by the cost calculator
42
+ startTime: request.startTime,
43
+ endTime: response.endTime,
44
+ durationMs: Math.max(0, duration),
45
+ cacheReadTokens: response.cacheReadTokens,
46
+ cacheCreationTokens: response.cacheCreationTokens,
47
+ telemetry: {
48
+ ...this.defaultContext,
49
+ ...request.telemetry
50
+ },
51
+ metadata: {
52
+ estimated: false
53
+ }
54
+ };
55
+ }
56
+ /**
57
+ * Emit a cost span
58
+ */
59
+ emitSpan(span) {
60
+ if (this.onSpanCallback) {
61
+ try {
62
+ this.onSpanCallback(span);
63
+ } catch {
64
+ }
65
+ }
66
+ }
67
+ /**
68
+ * Extract telemetry context from request options
69
+ */
70
+ extractTelemetryContext(options) {
71
+ const telemetry = options.telemetry;
72
+ if (telemetry && typeof telemetry === "object") {
73
+ const ctx = {};
74
+ if ("tenant" in telemetry && typeof telemetry.tenant === "string") {
75
+ ctx.tenant = telemetry.tenant;
76
+ }
77
+ if ("feature" in telemetry && typeof telemetry.feature === "string") {
78
+ ctx.feature = telemetry.feature;
79
+ }
80
+ if ("route" in telemetry && typeof telemetry.route === "string") {
81
+ ctx.route = telemetry.route;
82
+ }
83
+ return Object.keys(ctx).length > 0 ? ctx : void 0;
84
+ }
85
+ return void 0;
86
+ }
87
+ /**
88
+ * Dispose of the wrapper and release resources
89
+ */
90
+ dispose() {
91
+ this.onSpanCallback = null;
92
+ this.defaultContext = {};
93
+ }
94
+ /**
95
+ * Get the underlying client
96
+ */
97
+ unwrap() {
98
+ return this.client;
99
+ }
100
+ };
101
+
102
+ // src/openai.ts
103
+ import { now } from "@reaatech/llm-cost-telemetry";
104
+ var OpenAIWrapper = class extends BaseProviderWrapper {
105
+ /**
106
+ * Get the provider name
107
+ */
108
+ get provider() {
109
+ return "openai";
110
+ }
111
+ /**
112
+ * Wrap the OpenAI client to intercept chat completions
113
+ */
114
+ wrap() {
115
+ const originalClient = this.client;
116
+ const originalChatCreate = originalClient.chat.completions.create.bind(
117
+ originalClient.chat.completions
118
+ );
119
+ originalClient.chat.completions.create = (async (options, ...rest) => {
120
+ const startTime = now();
121
+ const telemetry = this.extractTelemetryContext(options);
122
+ const model = options.model;
123
+ const optionsObj = options;
124
+ const { telemetry: _, ...cleanOptionsObj } = optionsObj;
125
+ const cleanOptions = cleanOptionsObj;
126
+ try {
127
+ const response = await originalChatCreate(cleanOptions, ...rest);
128
+ const endTime = now();
129
+ const requestMetadata = {
130
+ model,
131
+ params: cleanOptions,
132
+ telemetry,
133
+ startTime
134
+ };
135
+ const responseMetadata = {
136
+ inputTokens: response.usage?.prompt_tokens ?? 0,
137
+ outputTokens: response.usage?.completion_tokens ?? 0,
138
+ endTime
139
+ };
140
+ const span = this.createSpan(requestMetadata, responseMetadata);
141
+ this.emitSpan(span);
142
+ return response;
143
+ } catch (error) {
144
+ const endTime = now();
145
+ const requestMetadata = {
146
+ model,
147
+ params: cleanOptions,
148
+ telemetry,
149
+ startTime
150
+ };
151
+ const responseMetadata = {
152
+ inputTokens: 0,
153
+ outputTokens: 0,
154
+ endTime,
155
+ error
156
+ };
157
+ const span = this.createSpan(requestMetadata, responseMetadata);
158
+ this.emitSpan(span);
159
+ throw error;
160
+ }
161
+ });
162
+ const originalCompletionCreate = originalClient.completions.create.bind(
163
+ originalClient.completions
164
+ );
165
+ originalClient.completions.create = (async (options, ...rest) => {
166
+ const startTime = now();
167
+ const telemetry = this.extractTelemetryContext(options);
168
+ const model = options.model;
169
+ const optionsObj = options;
170
+ const { telemetry: _, ...cleanOptionsObj } = optionsObj;
171
+ const cleanOptions = cleanOptionsObj;
172
+ try {
173
+ const response = await originalCompletionCreate(cleanOptions, ...rest);
174
+ const endTime = now();
175
+ const requestMetadata = {
176
+ model,
177
+ params: cleanOptions,
178
+ telemetry,
179
+ startTime
180
+ };
181
+ const responseMetadata = {
182
+ inputTokens: response.usage?.prompt_tokens ?? 0,
183
+ outputTokens: response.usage?.completion_tokens ?? 0,
184
+ endTime
185
+ };
186
+ const span = this.createSpan(requestMetadata, responseMetadata);
187
+ this.emitSpan(span);
188
+ return response;
189
+ } catch (error) {
190
+ const endTime = now();
191
+ const requestMetadata = {
192
+ model,
193
+ params: cleanOptions,
194
+ telemetry,
195
+ startTime
196
+ };
197
+ const responseMetadata = {
198
+ inputTokens: 0,
199
+ outputTokens: 0,
200
+ endTime,
201
+ error
202
+ };
203
+ const span = this.createSpan(requestMetadata, responseMetadata);
204
+ this.emitSpan(span);
205
+ throw error;
206
+ }
207
+ });
208
+ return originalClient;
209
+ }
210
+ };
211
+ function wrapOpenAI(client) {
212
+ const wrapper = new OpenAIWrapper(client);
213
+ return wrapper.wrap();
214
+ }
215
+
216
+ // src/anthropic.ts
217
+ import { now as now2 } from "@reaatech/llm-cost-telemetry";
218
+ var AnthropicWrapper = class extends BaseProviderWrapper {
219
+ /**
220
+ * Get the provider name
221
+ */
222
+ get provider() {
223
+ return "anthropic";
224
+ }
225
+ /**
226
+ * Wrap the Anthropic client to intercept messages.create
227
+ */
228
+ wrap() {
229
+ const originalClient = this.client;
230
+ const originalCreate = originalClient.messages.create.bind(originalClient.messages);
231
+ originalClient.messages.create = (async (options, ...rest) => {
232
+ const startTime = now2();
233
+ const telemetry = this.extractTelemetryContext(
234
+ options
235
+ );
236
+ const model = options.model;
237
+ const { telemetry: _, ...cleanOptions } = options;
238
+ try {
239
+ const response = await originalCreate(
240
+ cleanOptions,
241
+ ...rest
242
+ );
243
+ const endTime = now2();
244
+ const requestMetadata = {
245
+ model,
246
+ params: cleanOptions,
247
+ telemetry,
248
+ startTime
249
+ };
250
+ const inputTokens = response.usage.input_tokens ?? 0;
251
+ const outputTokens = response.usage.output_tokens ?? 0;
252
+ let cacheReadTokens;
253
+ let cacheCreationTokens;
254
+ const usage = response.usage;
255
+ if ("cache_read_input_tokens" in usage) {
256
+ cacheReadTokens = usage.cache_read_input_tokens;
257
+ }
258
+ if ("cache_creation_input_tokens" in usage) {
259
+ cacheCreationTokens = usage.cache_creation_input_tokens;
260
+ }
261
+ const responseMetadata = {
262
+ inputTokens,
263
+ outputTokens,
264
+ cacheReadTokens,
265
+ cacheCreationTokens,
266
+ endTime
267
+ };
268
+ const span = this.createSpan(requestMetadata, responseMetadata);
269
+ this.emitSpan(span);
270
+ return response;
271
+ } catch (error) {
272
+ const endTime = now2();
273
+ const requestMetadata = {
274
+ model,
275
+ params: cleanOptions,
276
+ telemetry,
277
+ startTime
278
+ };
279
+ const responseMetadata = {
280
+ inputTokens: 0,
281
+ outputTokens: 0,
282
+ endTime,
283
+ error
284
+ };
285
+ const span = this.createSpan(requestMetadata, responseMetadata);
286
+ this.emitSpan(span);
287
+ throw error;
288
+ }
289
+ });
290
+ return originalClient;
291
+ }
292
+ };
293
+ function wrapAnthropic(client) {
294
+ const wrapper = new AnthropicWrapper(client);
295
+ return wrapper.wrap();
296
+ }
297
+
298
+ // src/google.ts
299
+ import { now as now3 } from "@reaatech/llm-cost-telemetry";
300
+ var GoogleGenerativeAIWrapper = class extends BaseProviderWrapper {
301
+ /**
302
+ * Get the provider name
303
+ */
304
+ get provider() {
305
+ return "google";
306
+ }
307
+ /**
308
+ * Wrap the GoogleGenerativeAI client to intercept generateContent
309
+ */
310
+ wrap() {
311
+ const wrapper = this;
312
+ const originalClient = this.client;
313
+ const originalGetModel = originalClient.getGenerativeModel.bind(originalClient);
314
+ originalClient.getGenerativeModel = ((modelParams, ...rest) => {
315
+ const model = originalGetModel(modelParams, ...rest);
316
+ const originalGenerate = model.generateContent.bind(model);
317
+ model.generateContent = (async (request, options) => {
318
+ const startTime = now3();
319
+ const telemetry = options?.telemetry ? wrapper.extractTelemetryContext(options.telemetry) : void 0;
320
+ const modelId = modelParams.model;
321
+ try {
322
+ const response = await originalGenerate(request, options);
323
+ const endTime = now3();
324
+ const requestMetadata = {
325
+ model: modelId,
326
+ params: typeof request === "string" ? { prompt: request } : request,
327
+ telemetry,
328
+ startTime
329
+ };
330
+ const responseAny = response;
331
+ const responseMetadata = {
332
+ inputTokens: responseAny.usageMetadata?.promptTokenCount ?? 0,
333
+ outputTokens: responseAny.usageMetadata?.candidatesTokenCount ?? 0,
334
+ endTime
335
+ };
336
+ const span = wrapper.createSpan(requestMetadata, responseMetadata);
337
+ wrapper.emitSpan(span);
338
+ return response;
339
+ } catch (error) {
340
+ const endTime = now3();
341
+ const requestMetadata = {
342
+ model: modelId,
343
+ params: typeof request === "string" ? { prompt: request } : request,
344
+ telemetry,
345
+ startTime
346
+ };
347
+ const responseMetadata = {
348
+ inputTokens: 0,
349
+ outputTokens: 0,
350
+ endTime,
351
+ error
352
+ };
353
+ const span = wrapper.createSpan(requestMetadata, responseMetadata);
354
+ wrapper.emitSpan(span);
355
+ throw error;
356
+ }
357
+ });
358
+ const originalGenerateStream = model.generateContentStream.bind(model);
359
+ model.generateContentStream = (async (request, options) => {
360
+ const startTime = now3();
361
+ const telemetry = options?.telemetry ? wrapper.extractTelemetryContext(options.telemetry) : void 0;
362
+ const modelId = modelParams.model;
363
+ const responseStream = await originalGenerateStream(request, options);
364
+ const originalStream = responseStream.stream;
365
+ let totalInputTokens = 0;
366
+ let totalOutputTokens = 0;
367
+ const wrappedStream = new ReadableStream({
368
+ async start(controller) {
369
+ try {
370
+ for await (const chunk of originalStream) {
371
+ if (chunk.usageMetadata) {
372
+ totalInputTokens = chunk.usageMetadata.promptTokenCount ?? totalInputTokens;
373
+ totalOutputTokens = chunk.usageMetadata.candidatesTokenCount ?? totalOutputTokens;
374
+ }
375
+ controller.enqueue(chunk);
376
+ }
377
+ const endTime = now3();
378
+ const requestMetadata = {
379
+ model: modelId,
380
+ params: typeof request === "string" ? { prompt: request } : request,
381
+ telemetry,
382
+ startTime
383
+ };
384
+ const responseMetadata = {
385
+ inputTokens: totalInputTokens,
386
+ outputTokens: totalOutputTokens,
387
+ endTime
388
+ };
389
+ const span = wrapper.createSpan(requestMetadata, responseMetadata);
390
+ wrapper.emitSpan(span);
391
+ controller.close();
392
+ } catch (error) {
393
+ const endTime = now3();
394
+ const requestMetadata = {
395
+ model: modelId,
396
+ params: typeof request === "string" ? { prompt: request } : request,
397
+ telemetry,
398
+ startTime
399
+ };
400
+ const responseMetadata = {
401
+ inputTokens: 0,
402
+ outputTokens: 0,
403
+ endTime,
404
+ error
405
+ };
406
+ const span = wrapper.createSpan(requestMetadata, responseMetadata);
407
+ wrapper.emitSpan(span);
408
+ controller.error(error);
409
+ }
410
+ }
411
+ });
412
+ return {
413
+ stream: wrappedStream,
414
+ response: responseStream.response
415
+ };
416
+ });
417
+ return model;
418
+ });
419
+ return originalClient;
420
+ }
421
+ };
422
+ function wrapGoogleGenerativeAI(client) {
423
+ const wrapper = new GoogleGenerativeAIWrapper(client);
424
+ return wrapper.wrap();
425
+ }
426
+ export {
427
+ AnthropicWrapper,
428
+ BaseProviderWrapper,
429
+ GoogleGenerativeAIWrapper,
430
+ OpenAIWrapper,
431
+ wrapAnthropic,
432
+ wrapGoogleGenerativeAI,
433
+ wrapOpenAI
434
+ };
package/package.json ADDED
@@ -0,0 +1,63 @@
1
+ {
2
+ "name": "@reaatech/llm-cost-telemetry-providers",
3
+ "version": "0.1.0",
4
+ "description": "LLM provider SDK wrappers — OpenAI, Anthropic, and Google Generative AI",
5
+ "license": "MIT",
6
+ "author": "Rick Somers <rick@reaatech.com> (https://reaatech.com)",
7
+ "repository": {
8
+ "type": "git",
9
+ "url": "https://github.com/reaatech/llm-cost-telemetry.git",
10
+ "directory": "packages/providers"
11
+ },
12
+ "homepage": "https://github.com/reaatech/llm-cost-telemetry/tree/main/packages/providers#readme",
13
+ "bugs": {
14
+ "url": "https://github.com/reaatech/llm-cost-telemetry/issues"
15
+ },
16
+ "type": "module",
17
+ "main": "./dist/index.cjs",
18
+ "module": "./dist/index.js",
19
+ "types": "./dist/index.d.ts",
20
+ "exports": {
21
+ ".": {
22
+ "types": "./dist/index.d.ts",
23
+ "import": "./dist/index.js",
24
+ "require": "./dist/index.cjs"
25
+ }
26
+ },
27
+ "files": [
28
+ "dist"
29
+ ],
30
+ "publishConfig": {
31
+ "access": "public"
32
+ },
33
+ "dependencies": {
34
+ "@reaatech/llm-cost-telemetry": "0.1.0"
35
+ },
36
+ "peerDependencies": {
37
+ "@anthropic-ai/sdk": "^0.24.0",
38
+ "@google/generative-ai": "^0.21.0",
39
+ "openai": "^4.52.0"
40
+ },
41
+ "peerDependenciesMeta": {
42
+ "@anthropic-ai/sdk": {
43
+ "optional": true
44
+ },
45
+ "@google/generative-ai": {
46
+ "optional": true
47
+ },
48
+ "openai": {
49
+ "optional": true
50
+ }
51
+ },
52
+ "devDependencies": {
53
+ "tsup": "^8.4.0",
54
+ "typescript": "^5.8.3",
55
+ "vitest": "^3.1.1"
56
+ },
57
+ "scripts": {
58
+ "build": "tsup src/index.ts --format cjs,esm --dts --clean",
59
+ "test": "vitest run",
60
+ "test:coverage": "vitest run --coverage",
61
+ "clean": "rm -rf dist"
62
+ }
63
+ }