@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/LICENSE +21 -0
- package/README.md +205 -0
- package/dist/index.cjs +467 -0
- package/dist/index.d.cts +193 -0
- package/dist/index.d.ts +193 -0
- package/dist/index.js +434 -0
- package/package.json +63 -0
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
|
+
}
|