llm-burn 0.0.1 → 1.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/README.md +423 -1
- package/dist/constants.d.ts +2 -0
- package/dist/constants.js +6 -0
- package/dist/constants.js.map +1 -0
- package/dist/decorators/track-llm-burn.decorator.d.ts +13 -0
- package/dist/decorators/track-llm-burn.decorator.js +10 -0
- package/dist/decorators/track-llm-burn.decorator.js.map +1 -0
- package/dist/guards/budget.guard.d.ts +10 -0
- package/dist/guards/budget.guard.js +55 -0
- package/dist/guards/budget.guard.js.map +1 -0
- package/dist/index.d.ts +9 -0
- package/dist/index.js +17 -0
- package/dist/index.js.map +1 -0
- package/dist/interfaces/llm-burn-module-options.interface.d.ts +17 -0
- package/dist/interfaces/llm-burn-module-options.interface.js +3 -0
- package/dist/interfaces/llm-burn-module-options.interface.js.map +1 -0
- package/dist/interfaces/llm-usage.interface.d.ts +38 -0
- package/dist/interfaces/llm-usage.interface.js +3 -0
- package/dist/interfaces/llm-usage.interface.js.map +1 -0
- package/dist/llm-burn.interceptor.d.ts +13 -0
- package/dist/llm-burn.interceptor.js +151 -0
- package/dist/llm-burn.interceptor.js.map +1 -0
- package/dist/llm-burn.module.d.ts +7 -0
- package/dist/llm-burn.module.js +63 -0
- package/dist/llm-burn.module.js.map +1 -0
- package/dist/llm-burn.service.d.ts +20 -0
- package/dist/llm-burn.service.js +175 -0
- package/dist/llm-burn.service.js.map +1 -0
- package/dist/prices.json +216 -0
- package/dist/tsconfig.build.tsbuildinfo +1 -0
- package/package.json +61 -5
- package/index.js +0 -1
package/README.md
CHANGED
|
@@ -1,2 +1,424 @@
|
|
|
1
1
|
# llm-burn
|
|
2
|
-
|
|
2
|
+
|
|
3
|
+
[](https://www.npmjs.com/package/llm-burn)
|
|
4
|
+
[](LICENSE)
|
|
5
|
+
[](https://github.com/julioxavier/llm-burn)
|
|
6
|
+
|
|
7
|
+
**Cost tracking and budget enforcement for LLM calls in NestJS — with a single decorator.**
|
|
8
|
+
|
|
9
|
+
Every OpenAI and Anthropic call burns money. `llm-burn` tells you exactly how much, per method, per model, in real time — and optionally blocks requests once a spending limit is reached. Zero changes to your business logic required.
|
|
10
|
+
|
|
11
|
+
---
|
|
12
|
+
|
|
13
|
+
## Features
|
|
14
|
+
|
|
15
|
+
- **One decorator** — `@TrackLLMBurn()` is all you need to start tracking a method
|
|
16
|
+
- **Auto-detection** — parses OpenAI, Anthropic, flat, and LangChain response shapes out of the box
|
|
17
|
+
- **Budget enforcement** — `BudgetGuard` throws HTTP 403 before the LLM call is made when a cap is exceeded
|
|
18
|
+
- **Per-method and global budgets** — granular control over individual methods or the entire application
|
|
19
|
+
- **Built-in pricing** — ships with up-to-date prices for all major GPT and Claude models
|
|
20
|
+
- **Extensible** — override prices, add custom models, or write a custom extractor for any SDK
|
|
21
|
+
|
|
22
|
+
---
|
|
23
|
+
|
|
24
|
+
## Installation
|
|
25
|
+
|
|
26
|
+
```bash
|
|
27
|
+
npm install llm-burn
|
|
28
|
+
```
|
|
29
|
+
|
|
30
|
+
**Peer dependencies** (already present in any NestJS project):
|
|
31
|
+
|
|
32
|
+
```bash
|
|
33
|
+
npm install @nestjs/common @nestjs/core reflect-metadata rxjs
|
|
34
|
+
```
|
|
35
|
+
|
|
36
|
+
---
|
|
37
|
+
|
|
38
|
+
## Quick Start
|
|
39
|
+
|
|
40
|
+
**1. Register the module in `AppModule`:**
|
|
41
|
+
|
|
42
|
+
```typescript
|
|
43
|
+
import { LLMBurnModule } from 'llm-burn';
|
|
44
|
+
|
|
45
|
+
@Module({
|
|
46
|
+
imports: [
|
|
47
|
+
LLMBurnModule.forRoot({
|
|
48
|
+
globalBudget: 10.00, // block all LLM calls after $10 spent
|
|
49
|
+
enableLogging: true,
|
|
50
|
+
}),
|
|
51
|
+
],
|
|
52
|
+
})
|
|
53
|
+
export class AppModule {}
|
|
54
|
+
```
|
|
55
|
+
|
|
56
|
+
**2. Decorate the method that calls your LLM:**
|
|
57
|
+
|
|
58
|
+
```typescript
|
|
59
|
+
import { TrackLLMBurn } from 'llm-burn';
|
|
60
|
+
|
|
61
|
+
@Injectable()
|
|
62
|
+
export class AiService {
|
|
63
|
+
@TrackLLMBurn({ model: 'gpt-4o', budget: 2.00 })
|
|
64
|
+
async summarize(text: string) {
|
|
65
|
+
return this.openai.chat.completions.create({
|
|
66
|
+
model: 'gpt-4o',
|
|
67
|
+
messages: [{ role: 'user', content: text }],
|
|
68
|
+
});
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
```
|
|
72
|
+
|
|
73
|
+
**3. Query costs anywhere in your application:**
|
|
74
|
+
|
|
75
|
+
```typescript
|
|
76
|
+
@Injectable()
|
|
77
|
+
export class DashboardService {
|
|
78
|
+
constructor(private readonly llmBurn: LLMBurnService) {}
|
|
79
|
+
|
|
80
|
+
getReport() {
|
|
81
|
+
return this.llmBurn.getStats();
|
|
82
|
+
// { totalCost, totalCalls, byMethod, byModel, records }
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
```
|
|
86
|
+
|
|
87
|
+
That's it. Token usage is extracted automatically from the response — no wrappers, no interceptors to wire up manually.
|
|
88
|
+
|
|
89
|
+
---
|
|
90
|
+
|
|
91
|
+
## Module Registration
|
|
92
|
+
|
|
93
|
+
### Synchronous
|
|
94
|
+
|
|
95
|
+
```typescript
|
|
96
|
+
LLMBurnModule.forRoot({
|
|
97
|
+
globalBudget: 10.00,
|
|
98
|
+
enableLogging: true,
|
|
99
|
+
})
|
|
100
|
+
```
|
|
101
|
+
|
|
102
|
+
### Async (with ConfigService)
|
|
103
|
+
|
|
104
|
+
```typescript
|
|
105
|
+
LLMBurnModule.forRootAsync({
|
|
106
|
+
imports: [ConfigModule],
|
|
107
|
+
inject: [ConfigService],
|
|
108
|
+
useFactory: (cfg: ConfigService) => ({
|
|
109
|
+
globalBudget: cfg.get<number>('LLM_BUDGET'),
|
|
110
|
+
enableLogging: cfg.get<boolean>('LLM_LOGGING'),
|
|
111
|
+
}),
|
|
112
|
+
})
|
|
113
|
+
```
|
|
114
|
+
|
|
115
|
+
### With Global Interceptor
|
|
116
|
+
|
|
117
|
+
Registers `LLMBurnInterceptor` as an `APP_INTERCEPTOR` so every route in your application is automatically intercepted. Combine with `@TrackLLMBurn()` on specific methods to control what gets tracked.
|
|
118
|
+
|
|
119
|
+
```typescript
|
|
120
|
+
LLMBurnModule.forRootWithGlobalInterceptor({
|
|
121
|
+
globalBudget: 5.00,
|
|
122
|
+
enableLogging: true,
|
|
123
|
+
})
|
|
124
|
+
```
|
|
125
|
+
|
|
126
|
+
---
|
|
127
|
+
|
|
128
|
+
## Decorator: `@TrackLLMBurn`
|
|
129
|
+
|
|
130
|
+
Marks a method for LLM cost tracking. Automatically attaches the interceptor to that method — no need to wire up `UseInterceptors` manually.
|
|
131
|
+
|
|
132
|
+
```typescript
|
|
133
|
+
@TrackLLMBurn({ model: 'claude-3-5-sonnet-20241022', budget: 1.50 })
|
|
134
|
+
async generateReport(prompt: string) {
|
|
135
|
+
return this.anthropic.messages.create({
|
|
136
|
+
model: 'claude-3-5-sonnet-20241022',
|
|
137
|
+
max_tokens: 1024,
|
|
138
|
+
messages: [{ role: 'user', content: prompt }],
|
|
139
|
+
});
|
|
140
|
+
}
|
|
141
|
+
```
|
|
142
|
+
|
|
143
|
+
| Option | Type | Description |
|
|
144
|
+
|---|---|---|
|
|
145
|
+
| `model` | `string` | Model name used as fallback when the response doesn't include one. |
|
|
146
|
+
| `provider` | `string` | Provider hint (`"openai"`, `"anthropic"`, or custom). Auto-detected from model name when omitted. |
|
|
147
|
+
| `budget` | `number` | Per-method USD cap. `BudgetGuard` blocks calls once this is reached. |
|
|
148
|
+
| `extractUsage` | `(result: unknown) => ExtractedUsage \| null` | Custom extractor for non-standard response shapes. |
|
|
149
|
+
|
|
150
|
+
---
|
|
151
|
+
|
|
152
|
+
## Budget Guard
|
|
153
|
+
|
|
154
|
+
`BudgetGuard` runs **before** the route handler. It checks two thresholds in order:
|
|
155
|
+
|
|
156
|
+
1. **Per-method budget** — reads `budget` from `@TrackLLMBurn({ budget: N })` and compares it against the cumulative cost of all previous calls to that method.
|
|
157
|
+
2. **Global budget** — checks if `totalCost >= globalBudget` across all tracked calls.
|
|
158
|
+
|
|
159
|
+
If either threshold is exceeded, it throws `ForbiddenException` (HTTP 403) and the LLM call is never made.
|
|
160
|
+
|
|
161
|
+
> The guard checks cost accumulated from **previous calls**. The current call's cost is recorded after it completes (in the interceptor). This is by design — the guard acts as a spending limiter, not a per-call price check.
|
|
162
|
+
|
|
163
|
+
### Applying the guard
|
|
164
|
+
|
|
165
|
+
**Globally:**
|
|
166
|
+
|
|
167
|
+
```typescript
|
|
168
|
+
// main.ts
|
|
169
|
+
async function bootstrap() {
|
|
170
|
+
const app = await NestFactory.create(AppModule);
|
|
171
|
+
app.useGlobalGuards(app.get(BudgetGuard));
|
|
172
|
+
await app.listen(3000);
|
|
173
|
+
}
|
|
174
|
+
```
|
|
175
|
+
|
|
176
|
+
**Per controller:**
|
|
177
|
+
|
|
178
|
+
```typescript
|
|
179
|
+
@UseGuards(BudgetGuard)
|
|
180
|
+
@Controller('ai')
|
|
181
|
+
export class AiController {}
|
|
182
|
+
```
|
|
183
|
+
|
|
184
|
+
**Per route:**
|
|
185
|
+
|
|
186
|
+
```typescript
|
|
187
|
+
@UseGuards(BudgetGuard)
|
|
188
|
+
@Post('summarize')
|
|
189
|
+
@TrackLLMBurn({ model: 'gpt-4o', budget: 0.50 })
|
|
190
|
+
async summarize(@Body() dto: SummarizeDto) { ... }
|
|
191
|
+
```
|
|
192
|
+
|
|
193
|
+
> When `BudgetGuard` is not registered, no request is ever blocked. Cost tracking via the interceptor still works normally.
|
|
194
|
+
|
|
195
|
+
---
|
|
196
|
+
|
|
197
|
+
## LLMBurnService
|
|
198
|
+
|
|
199
|
+
Injectable service available anywhere after importing `LLMBurnModule`.
|
|
200
|
+
|
|
201
|
+
| Method | Return | Description |
|
|
202
|
+
|---|---|---|
|
|
203
|
+
| `getStats()` | `LLMStats` | Full breakdown: totals, per-method, per-model, raw records |
|
|
204
|
+
| `getTotalCost()` | `number` | Total USD spent across all calls |
|
|
205
|
+
| `getMethodCost(method)` | `number` | Cumulative USD cost for a specific method |
|
|
206
|
+
| `getBudgetStatus()` | `BudgetStatus` | Global budget usage (remaining, exceeded, % used) |
|
|
207
|
+
| `getGlobalBudget()` | `number \| undefined` | Configured global budget cap |
|
|
208
|
+
| `calculateCost(model, in, out, cached?)` | `number` | Calculate USD cost for a given token count |
|
|
209
|
+
| `getPricing(model)` | `ModelPricing \| undefined` | Retrieve pricing for a model (supports prefix matching) |
|
|
210
|
+
| `listKnownModels()` | `string[]` | All model names from built-in + custom prices |
|
|
211
|
+
| `record(method, model, provider, in, out)` | `LLMCallRecord` | Manually record a call |
|
|
212
|
+
| `reset()` | `void` | Clear all recorded usage |
|
|
213
|
+
|
|
214
|
+
**Example — cost dashboard endpoint:**
|
|
215
|
+
|
|
216
|
+
```typescript
|
|
217
|
+
@Get('cost-report')
|
|
218
|
+
getCostReport() {
|
|
219
|
+
return {
|
|
220
|
+
stats: this.llmBurn.getStats(),
|
|
221
|
+
budget: this.llmBurn.getBudgetStatus(),
|
|
222
|
+
};
|
|
223
|
+
}
|
|
224
|
+
```
|
|
225
|
+
|
|
226
|
+
---
|
|
227
|
+
|
|
228
|
+
## Supported Response Formats
|
|
229
|
+
|
|
230
|
+
The interceptor auto-detects seven response shapes out of the box:
|
|
231
|
+
|
|
232
|
+
| Format | Shape |
|
|
233
|
+
|---|---|
|
|
234
|
+
| **OpenAI SDK** | `{ usage: { prompt_tokens, completion_tokens }, model }` |
|
|
235
|
+
| **Anthropic SDK** | `{ usage: { input_tokens, output_tokens }, model }` |
|
|
236
|
+
| **Google Gemini SDK** | `{ usageMetadata: { promptTokenCount, candidatesTokenCount } }` |
|
|
237
|
+
| **Cohere SDK** | `{ meta: { tokens: { input_tokens, output_tokens } } }` |
|
|
238
|
+
| **Mistral SDK** | Same as OpenAI (auto-detected) |
|
|
239
|
+
| **Flat** | `{ inputTokens, outputTokens, model? }` |
|
|
240
|
+
| **LangChain** | `{ llmOutput: { tokenUsage: { promptTokens, completionTokens } } }` |
|
|
241
|
+
|
|
242
|
+
> **Groq, Together AI, Azure OpenAI** use the OpenAI SDK format and are detected automatically.
|
|
243
|
+
|
|
244
|
+
### Custom extractor
|
|
245
|
+
|
|
246
|
+
For non-standard SDKs or response wrappers, provide an `extractUsage` function:
|
|
247
|
+
|
|
248
|
+
```typescript
|
|
249
|
+
@TrackLLMBurn({
|
|
250
|
+
model: 'my-custom-model',
|
|
251
|
+
extractUsage: (result: unknown) => {
|
|
252
|
+
const r = result as MyCustomResponse;
|
|
253
|
+
if (!r?.meta?.tokens) return null;
|
|
254
|
+
return {
|
|
255
|
+
inputTokens: r.meta.tokens.input,
|
|
256
|
+
outputTokens: r.meta.tokens.output,
|
|
257
|
+
model: r.meta.model, // optional — overrides decorator model
|
|
258
|
+
provider: 'my-provider', // optional — overrides auto-detection
|
|
259
|
+
};
|
|
260
|
+
},
|
|
261
|
+
})
|
|
262
|
+
async callCustomLLM(prompt: string) { ... }
|
|
263
|
+
```
|
|
264
|
+
|
|
265
|
+
Return `null` to skip recording for a specific call — the interceptor will log a warning.
|
|
266
|
+
|
|
267
|
+
---
|
|
268
|
+
|
|
269
|
+
## Supported Models & Pricing
|
|
270
|
+
|
|
271
|
+
Prices are in USD per 1 million tokens (updated March 2026).
|
|
272
|
+
|
|
273
|
+
### OpenAI
|
|
274
|
+
|
|
275
|
+
| Model | Input / M | Output / M | Cached Input / M |
|
|
276
|
+
|---|---|---|---|
|
|
277
|
+
| `gpt-4o` | $2.50 | $10.00 | $1.25 |
|
|
278
|
+
| `gpt-4o-mini` | $0.15 | $0.60 | $0.075 |
|
|
279
|
+
| `gpt-4-turbo` | $10.00 | $30.00 | — |
|
|
280
|
+
| `gpt-4` | $30.00 | $60.00 | — |
|
|
281
|
+
| `gpt-3.5-turbo` | $0.50 | $1.50 | — |
|
|
282
|
+
| `o1` | $15.00 | $60.00 | $7.50 |
|
|
283
|
+
| `o1-mini` | $3.00 | $12.00 | $1.50 |
|
|
284
|
+
| `o3` | $10.00 | $40.00 | $2.50 |
|
|
285
|
+
| `o3-mini` | $1.10 | $4.40 | $0.55 |
|
|
286
|
+
| `gpt-4.5-preview` | $75.00 | $150.00 | $37.50 |
|
|
287
|
+
|
|
288
|
+
### Anthropic
|
|
289
|
+
|
|
290
|
+
| Model | Input / M | Output / M |
|
|
291
|
+
|---|---|---|
|
|
292
|
+
| `claude-opus-4-6` | $15.00 | $75.00 |
|
|
293
|
+
| `claude-sonnet-4-6` | $3.00 | $15.00 |
|
|
294
|
+
| `claude-haiku-4-5` | $0.80 | $4.00 |
|
|
295
|
+
| `claude-3-5-sonnet-20241022` | $3.00 | $15.00 |
|
|
296
|
+
| `claude-3-5-haiku-20241022` | $0.80 | $4.00 |
|
|
297
|
+
| `claude-3-opus-20240229` | $15.00 | $75.00 |
|
|
298
|
+
| `claude-3-haiku-20240307` | $0.25 | $1.25 |
|
|
299
|
+
| `claude-2.1` | $8.00 | $24.00 |
|
|
300
|
+
|
|
301
|
+
> **Prefix matching:** dated model variants like `gpt-4o-2024-11-20` are matched automatically — if no exact match exists, the interceptor falls back to the nearest prefix entry (`gpt-4o`).
|
|
302
|
+
|
|
303
|
+
### Google Gemini
|
|
304
|
+
|
|
305
|
+
| Model | Input / M | Output / M |
|
|
306
|
+
|---|---|---|
|
|
307
|
+
| `gemini-2.0-flash` | $0.10 | $0.40 |
|
|
308
|
+
| `gemini-2.0-flash-lite` | $0.075 | $0.30 |
|
|
309
|
+
| `gemini-1.5-pro` | $1.25 | $5.00 |
|
|
310
|
+
| `gemini-1.5-flash` | $0.075 | $0.30 |
|
|
311
|
+
| `gemini-1.5-flash-8b` | $0.0375 | $0.15 |
|
|
312
|
+
|
|
313
|
+
### Cohere
|
|
314
|
+
|
|
315
|
+
| Model | Input / M | Output / M |
|
|
316
|
+
|---|---|---|
|
|
317
|
+
| `command-r-plus` | $2.50 | $10.00 |
|
|
318
|
+
| `command-r` | $0.15 | $0.60 |
|
|
319
|
+
| `command` | $1.00 | $2.00 |
|
|
320
|
+
| `command-light` | $0.30 | $0.60 |
|
|
321
|
+
|
|
322
|
+
### Mistral
|
|
323
|
+
|
|
324
|
+
| Model | Input / M | Output / M |
|
|
325
|
+
|---|---|---|
|
|
326
|
+
| `mistral-large-latest` | $2.00 | $6.00 |
|
|
327
|
+
| `mistral-small-latest` | $0.10 | $0.30 |
|
|
328
|
+
| `codestral-latest` | $0.20 | $0.60 |
|
|
329
|
+
| `open-mistral-nemo` | $0.15 | $0.15 |
|
|
330
|
+
| `open-mixtral-8x22b` | $2.00 | $6.00 |
|
|
331
|
+
|
|
332
|
+
### Custom Prices
|
|
333
|
+
|
|
334
|
+
**Inline** — add or override any model price at module registration:
|
|
335
|
+
|
|
336
|
+
```typescript
|
|
337
|
+
LLMBurnModule.forRoot({
|
|
338
|
+
customPrices: {
|
|
339
|
+
'my-fine-tuned-gpt4': {
|
|
340
|
+
inputPricePerMillion: 5.00,
|
|
341
|
+
outputPricePerMillion: 20.00,
|
|
342
|
+
},
|
|
343
|
+
'local-llama': {
|
|
344
|
+
inputPricePerMillion: 0,
|
|
345
|
+
outputPricePerMillion: 0,
|
|
346
|
+
},
|
|
347
|
+
},
|
|
348
|
+
})
|
|
349
|
+
```
|
|
350
|
+
|
|
351
|
+
**External file** — point to your own JSON file to manage prices independently of the package version:
|
|
352
|
+
|
|
353
|
+
```typescript
|
|
354
|
+
LLMBurnModule.forRoot({
|
|
355
|
+
pricesPath: './prices.json',
|
|
356
|
+
})
|
|
357
|
+
```
|
|
358
|
+
|
|
359
|
+
The file can be flat or nested (same shape as the built-in `prices.json`):
|
|
360
|
+
|
|
361
|
+
```json
|
|
362
|
+
// flat
|
|
363
|
+
{
|
|
364
|
+
"gpt-4o": { "inputPricePerMillion": 2.50, "outputPricePerMillion": 10.00 },
|
|
365
|
+
"claude-sonnet-4-6": { "inputPricePerMillion": 3.00, "outputPricePerMillion": 15.00 }
|
|
366
|
+
}
|
|
367
|
+
|
|
368
|
+
// nested (grouped by provider)
|
|
369
|
+
{
|
|
370
|
+
"openai": {
|
|
371
|
+
"gpt-4o": { "inputPricePerMillion": 2.50, "outputPricePerMillion": 10.00 }
|
|
372
|
+
},
|
|
373
|
+
"anthropic": {
|
|
374
|
+
"claude-sonnet-4-6": { "inputPricePerMillion": 3.00, "outputPricePerMillion": 15.00 }
|
|
375
|
+
}
|
|
376
|
+
}
|
|
377
|
+
```
|
|
378
|
+
|
|
379
|
+
**Priority order:** `customPrices` > `pricesPath` > built-in prices.
|
|
380
|
+
|
|
381
|
+
---
|
|
382
|
+
|
|
383
|
+
## API Reference
|
|
384
|
+
|
|
385
|
+
### LLMBurnModuleOptions
|
|
386
|
+
|
|
387
|
+
| Option | Type | Default | Description |
|
|
388
|
+
|---|---|---|---|
|
|
389
|
+
| `globalBudget` | `number` | — | USD cap for total spend. `BudgetGuard` blocks when exceeded. |
|
|
390
|
+
| `enableLogging` | `boolean` | `false` | Log each tracked call via NestJS Logger. |
|
|
391
|
+
| `customPrices` | `Record<string, ModelPricing>` | — | Inline price overrides. Takes precedence over everything. |
|
|
392
|
+
| `pricesPath` | `string` | — | Path to an external JSON prices file. Takes precedence over built-in prices. |
|
|
393
|
+
|
|
394
|
+
### LLMStats
|
|
395
|
+
|
|
396
|
+
```typescript
|
|
397
|
+
interface LLMStats {
|
|
398
|
+
totalCost: number;
|
|
399
|
+
totalInputTokens: number;
|
|
400
|
+
totalOutputTokens: number;
|
|
401
|
+
totalCalls: number;
|
|
402
|
+
byMethod: Record<string, MethodStats>; // breakdown per decorated method
|
|
403
|
+
byModel: Record<string, ModelStats>; // breakdown per model name
|
|
404
|
+
records: LLMCallRecord[]; // all raw records
|
|
405
|
+
}
|
|
406
|
+
```
|
|
407
|
+
|
|
408
|
+
### BudgetStatus
|
|
409
|
+
|
|
410
|
+
```typescript
|
|
411
|
+
interface BudgetStatus {
|
|
412
|
+
globalBudget?: number; // configured cap (undefined if not set)
|
|
413
|
+
totalCost: number; // total USD spent
|
|
414
|
+
remaining?: number; // USD left before cap (0 when exceeded)
|
|
415
|
+
isExceeded: boolean;
|
|
416
|
+
percentUsed?: number; // 0–100+
|
|
417
|
+
}
|
|
418
|
+
```
|
|
419
|
+
|
|
420
|
+
---
|
|
421
|
+
|
|
422
|
+
## License
|
|
423
|
+
|
|
424
|
+
MIT
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.LLM_BURN_METADATA = exports.LLM_BURN_OPTIONS = void 0;
|
|
4
|
+
exports.LLM_BURN_OPTIONS = 'LLM_BURN_OPTIONS';
|
|
5
|
+
exports.LLM_BURN_METADATA = 'llm_burn:track';
|
|
6
|
+
//# sourceMappingURL=constants.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"constants.js","sourceRoot":"","sources":["../src/constants.ts"],"names":[],"mappings":";;;AAAa,QAAA,gBAAgB,GAAG,kBAAkB,CAAC;AACtC,QAAA,iBAAiB,GAAG,gBAAgB,CAAC"}
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
export interface TrackLLMBurnOptions {
|
|
2
|
+
model?: string;
|
|
3
|
+
provider?: string;
|
|
4
|
+
budget?: number;
|
|
5
|
+
extractUsage?: (result: unknown) => ExtractedUsage | null;
|
|
6
|
+
}
|
|
7
|
+
export interface ExtractedUsage {
|
|
8
|
+
inputTokens: number;
|
|
9
|
+
outputTokens: number;
|
|
10
|
+
model?: string;
|
|
11
|
+
provider?: string;
|
|
12
|
+
}
|
|
13
|
+
export declare function TrackLLMBurn(options?: TrackLLMBurnOptions): MethodDecorator;
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.TrackLLMBurn = TrackLLMBurn;
|
|
4
|
+
const common_1 = require("@nestjs/common");
|
|
5
|
+
const constants_1 = require("../constants");
|
|
6
|
+
const llm_burn_interceptor_1 = require("../llm-burn.interceptor");
|
|
7
|
+
function TrackLLMBurn(options = {}) {
|
|
8
|
+
return (0, common_1.applyDecorators)((0, common_1.SetMetadata)(constants_1.LLM_BURN_METADATA, options), (0, common_1.UseInterceptors)(llm_burn_interceptor_1.LLMBurnInterceptor));
|
|
9
|
+
}
|
|
10
|
+
//# sourceMappingURL=track-llm-burn.decorator.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"track-llm-burn.decorator.js","sourceRoot":"","sources":["../../src/decorators/track-llm-burn.decorator.ts"],"names":[],"mappings":";;AAoDA,oCAKC;AAzDD,2CAA+E;AAC/E,4CAAiD;AACjD,kEAA6D;AAkD7D,SAAgB,YAAY,CAAC,UAA+B,EAAE;IAC5D,OAAO,IAAA,wBAAe,EACpB,IAAA,oBAAW,EAAC,6BAAiB,EAAE,OAAO,CAAC,EACvC,IAAA,wBAAe,EAAC,yCAAkB,CAAC,CACpC,CAAC;AACJ,CAAC"}
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
import { CanActivate, ExecutionContext } from '@nestjs/common';
|
|
2
|
+
import { Reflector } from '@nestjs/core';
|
|
3
|
+
import { LLMBurnService } from '../llm-burn.service';
|
|
4
|
+
export declare class BudgetGuard implements CanActivate {
|
|
5
|
+
private readonly llmBurnService;
|
|
6
|
+
private readonly reflector;
|
|
7
|
+
private readonly logger;
|
|
8
|
+
constructor(llmBurnService: LLMBurnService, reflector: Reflector);
|
|
9
|
+
canActivate(context: ExecutionContext): boolean;
|
|
10
|
+
}
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __decorate = (this && this.__decorate) || function (decorators, target, key, desc) {
|
|
3
|
+
var c = arguments.length, r = c < 3 ? target : desc === null ? desc = Object.getOwnPropertyDescriptor(target, key) : desc, d;
|
|
4
|
+
if (typeof Reflect === "object" && typeof Reflect.decorate === "function") r = Reflect.decorate(decorators, target, key, desc);
|
|
5
|
+
else for (var i = decorators.length - 1; i >= 0; i--) if (d = decorators[i]) r = (c < 3 ? d(r) : c > 3 ? d(target, key, r) : d(target, key)) || r;
|
|
6
|
+
return c > 3 && r && Object.defineProperty(target, key, r), r;
|
|
7
|
+
};
|
|
8
|
+
var __metadata = (this && this.__metadata) || function (k, v) {
|
|
9
|
+
if (typeof Reflect === "object" && typeof Reflect.metadata === "function") return Reflect.metadata(k, v);
|
|
10
|
+
};
|
|
11
|
+
var BudgetGuard_1;
|
|
12
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
13
|
+
exports.BudgetGuard = void 0;
|
|
14
|
+
const common_1 = require("@nestjs/common");
|
|
15
|
+
const core_1 = require("@nestjs/core");
|
|
16
|
+
const constants_1 = require("../constants");
|
|
17
|
+
const llm_burn_service_1 = require("../llm-burn.service");
|
|
18
|
+
let BudgetGuard = BudgetGuard_1 = class BudgetGuard {
|
|
19
|
+
constructor(llmBurnService, reflector) {
|
|
20
|
+
this.llmBurnService = llmBurnService;
|
|
21
|
+
this.reflector = reflector;
|
|
22
|
+
this.logger = new common_1.Logger(BudgetGuard_1.name);
|
|
23
|
+
}
|
|
24
|
+
canActivate(context) {
|
|
25
|
+
const options = this.reflector.getAllAndOverride(constants_1.LLM_BURN_METADATA, [
|
|
26
|
+
context.getHandler(),
|
|
27
|
+
context.getClass(),
|
|
28
|
+
]);
|
|
29
|
+
if (options?.budget != null) {
|
|
30
|
+
const methodName = context.getHandler().name;
|
|
31
|
+
const methodCost = this.llmBurnService.getMethodCost(methodName);
|
|
32
|
+
if (methodCost >= options.budget) {
|
|
33
|
+
const msg = `Method "${methodName}" has exceeded its budget cap of $${options.budget.toFixed(4)} ` +
|
|
34
|
+
`(current: $${methodCost.toFixed(4)}).`;
|
|
35
|
+
this.logger.warn(msg);
|
|
36
|
+
throw new common_1.ForbiddenException(msg);
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
const status = this.llmBurnService.getBudgetStatus();
|
|
40
|
+
if (status.isExceeded) {
|
|
41
|
+
const msg = `Global LLM budget of $${status.globalBudget.toFixed(4)} exceeded ` +
|
|
42
|
+
`(spent: $${status.totalCost.toFixed(4)}).`;
|
|
43
|
+
this.logger.warn(msg);
|
|
44
|
+
throw new common_1.ForbiddenException(msg);
|
|
45
|
+
}
|
|
46
|
+
return true;
|
|
47
|
+
}
|
|
48
|
+
};
|
|
49
|
+
exports.BudgetGuard = BudgetGuard;
|
|
50
|
+
exports.BudgetGuard = BudgetGuard = BudgetGuard_1 = __decorate([
|
|
51
|
+
(0, common_1.Injectable)(),
|
|
52
|
+
__metadata("design:paramtypes", [llm_burn_service_1.LLMBurnService,
|
|
53
|
+
core_1.Reflector])
|
|
54
|
+
], BudgetGuard);
|
|
55
|
+
//# sourceMappingURL=budget.guard.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"budget.guard.js","sourceRoot":"","sources":["../../src/guards/budget.guard.ts"],"names":[],"mappings":";;;;;;;;;;;;;AAAA,2CAMwB;AACxB,uCAAyC;AACzC,4CAAiD;AAEjD,0DAAqD;AAqB9C,IAAM,WAAW,mBAAjB,MAAM,WAAW;IAGtB,YACmB,cAA8B,EAC9B,SAAoB;QADpB,mBAAc,GAAd,cAAc,CAAgB;QAC9B,cAAS,GAAT,SAAS,CAAW;QAJtB,WAAM,GAAG,IAAI,eAAM,CAAC,aAAW,CAAC,IAAI,CAAC,CAAC;IAKpD,CAAC;IAEJ,WAAW,CAAC,OAAyB;QACnC,MAAM,OAAO,GAAG,IAAI,CAAC,SAAS,CAAC,iBAAiB,CAAsB,6BAAiB,EAAE;YACvF,OAAO,CAAC,UAAU,EAAE;YACpB,OAAO,CAAC,QAAQ,EAAE;SACnB,CAAC,CAAC;QAGH,IAAI,OAAO,EAAE,MAAM,IAAI,IAAI,EAAE,CAAC;YAC5B,MAAM,UAAU,GAAG,OAAO,CAAC,UAAU,EAAE,CAAC,IAAI,CAAC;YAC7C,MAAM,UAAU,GAAG,IAAI,CAAC,cAAc,CAAC,aAAa,CAAC,UAAU,CAAC,CAAC;YAEjE,IAAI,UAAU,IAAI,OAAO,CAAC,MAAM,EAAE,CAAC;gBACjC,MAAM,GAAG,GACP,WAAW,UAAU,qCAAqC,OAAO,CAAC,MAAM,CAAC,OAAO,CAAC,CAAC,CAAC,GAAG;oBACtF,cAAc,UAAU,CAAC,OAAO,CAAC,CAAC,CAAC,IAAI,CAAC;gBAC1C,IAAI,CAAC,MAAM,CAAC,IAAI,CAAC,GAAG,CAAC,CAAC;gBACtB,MAAM,IAAI,2BAAkB,CAAC,GAAG,CAAC,CAAC;YACpC,CAAC;QACH,CAAC;QAGD,MAAM,MAAM,GAAG,IAAI,CAAC,cAAc,CAAC,eAAe,EAAE,CAAC;QACrD,IAAI,MAAM,CAAC,UAAU,EAAE,CAAC;YACtB,MAAM,GAAG,GACP,yBAAyB,MAAM,CAAC,YAAa,CAAC,OAAO,CAAC,CAAC,CAAC,YAAY;gBACpE,YAAY,MAAM,CAAC,SAAS,CAAC,OAAO,CAAC,CAAC,CAAC,IAAI,CAAC;YAC9C,IAAI,CAAC,MAAM,CAAC,IAAI,CAAC,GAAG,CAAC,CAAC;YACtB,MAAM,IAAI,2BAAkB,CAAC,GAAG,CAAC,CAAC;QACpC,CAAC;QAED,OAAO,IAAI,CAAC;IACd,CAAC;CACF,CAAA;AAxCY,kCAAW;sBAAX,WAAW;IADvB,IAAA,mBAAU,GAAE;qCAKwB,iCAAc;QACnB,gBAAS;GAL5B,WAAW,CAwCvB"}
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
export { LLMBurnModule } from './llm-burn.module';
|
|
2
|
+
export { LLMBurnService } from './llm-burn.service';
|
|
3
|
+
export { LLMBurnInterceptor } from './llm-burn.interceptor';
|
|
4
|
+
export { BudgetGuard } from './guards/budget.guard';
|
|
5
|
+
export { TrackLLMBurn } from './decorators/track-llm-burn.decorator';
|
|
6
|
+
export type { TrackLLMBurnOptions, ExtractedUsage } from './decorators/track-llm-burn.decorator';
|
|
7
|
+
export type { LLMBurnModuleOptions, LLMBurnModuleAsyncOptions, ModelPricing, } from './interfaces/llm-burn-module-options.interface';
|
|
8
|
+
export type { LLMCallRecord, LLMStats, MethodStats, ModelStats, BudgetStatus, } from './interfaces/llm-usage.interface';
|
|
9
|
+
export { LLM_BURN_OPTIONS, LLM_BURN_METADATA } from './constants';
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.LLM_BURN_METADATA = exports.LLM_BURN_OPTIONS = exports.TrackLLMBurn = exports.BudgetGuard = exports.LLMBurnInterceptor = exports.LLMBurnService = exports.LLMBurnModule = void 0;
|
|
4
|
+
var llm_burn_module_1 = require("./llm-burn.module");
|
|
5
|
+
Object.defineProperty(exports, "LLMBurnModule", { enumerable: true, get: function () { return llm_burn_module_1.LLMBurnModule; } });
|
|
6
|
+
var llm_burn_service_1 = require("./llm-burn.service");
|
|
7
|
+
Object.defineProperty(exports, "LLMBurnService", { enumerable: true, get: function () { return llm_burn_service_1.LLMBurnService; } });
|
|
8
|
+
var llm_burn_interceptor_1 = require("./llm-burn.interceptor");
|
|
9
|
+
Object.defineProperty(exports, "LLMBurnInterceptor", { enumerable: true, get: function () { return llm_burn_interceptor_1.LLMBurnInterceptor; } });
|
|
10
|
+
var budget_guard_1 = require("./guards/budget.guard");
|
|
11
|
+
Object.defineProperty(exports, "BudgetGuard", { enumerable: true, get: function () { return budget_guard_1.BudgetGuard; } });
|
|
12
|
+
var track_llm_burn_decorator_1 = require("./decorators/track-llm-burn.decorator");
|
|
13
|
+
Object.defineProperty(exports, "TrackLLMBurn", { enumerable: true, get: function () { return track_llm_burn_decorator_1.TrackLLMBurn; } });
|
|
14
|
+
var constants_1 = require("./constants");
|
|
15
|
+
Object.defineProperty(exports, "LLM_BURN_OPTIONS", { enumerable: true, get: function () { return constants_1.LLM_BURN_OPTIONS; } });
|
|
16
|
+
Object.defineProperty(exports, "LLM_BURN_METADATA", { enumerable: true, get: function () { return constants_1.LLM_BURN_METADATA; } });
|
|
17
|
+
//# sourceMappingURL=index.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":";;;AACA,qDAAkD;AAAzC,gHAAA,aAAa,OAAA;AAGtB,uDAAoD;AAA3C,kHAAA,cAAc,OAAA;AAGvB,+DAA4D;AAAnD,0HAAA,kBAAkB,OAAA;AAG3B,sDAAoD;AAA3C,2GAAA,WAAW,OAAA;AAGpB,kFAAqE;AAA5D,wHAAA,YAAY,OAAA;AAmBrB,yCAAkE;AAAzD,6GAAA,gBAAgB,OAAA;AAAE,8GAAA,iBAAiB,OAAA"}
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
export interface ModelPricing {
|
|
2
|
+
inputPricePerMillion: number;
|
|
3
|
+
outputPricePerMillion: number;
|
|
4
|
+
cachedInputPricePerMillion?: number;
|
|
5
|
+
}
|
|
6
|
+
export interface LLMBurnModuleOptions {
|
|
7
|
+
globalBudget?: number;
|
|
8
|
+
throwOnBudgetExceeded?: boolean;
|
|
9
|
+
customPrices?: Record<string, ModelPricing>;
|
|
10
|
+
pricesPath?: string;
|
|
11
|
+
enableLogging?: boolean;
|
|
12
|
+
}
|
|
13
|
+
export interface LLMBurnModuleAsyncOptions {
|
|
14
|
+
imports?: any[];
|
|
15
|
+
useFactory: (...args: any[]) => LLMBurnModuleOptions | Promise<LLMBurnModuleOptions>;
|
|
16
|
+
inject?: any[];
|
|
17
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"llm-burn-module-options.interface.js","sourceRoot":"","sources":["../../src/interfaces/llm-burn-module-options.interface.ts"],"names":[],"mappings":""}
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
export interface LLMCallRecord {
|
|
2
|
+
method: string;
|
|
3
|
+
model: string;
|
|
4
|
+
provider: string;
|
|
5
|
+
inputTokens: number;
|
|
6
|
+
outputTokens: number;
|
|
7
|
+
cost: number;
|
|
8
|
+
timestamp: Date;
|
|
9
|
+
}
|
|
10
|
+
export interface MethodStats {
|
|
11
|
+
totalCost: number;
|
|
12
|
+
totalCalls: number;
|
|
13
|
+
totalInputTokens: number;
|
|
14
|
+
totalOutputTokens: number;
|
|
15
|
+
lastCalledAt?: Date;
|
|
16
|
+
}
|
|
17
|
+
export interface ModelStats {
|
|
18
|
+
totalCost: number;
|
|
19
|
+
totalCalls: number;
|
|
20
|
+
totalInputTokens: number;
|
|
21
|
+
totalOutputTokens: number;
|
|
22
|
+
}
|
|
23
|
+
export interface LLMStats {
|
|
24
|
+
totalCost: number;
|
|
25
|
+
totalInputTokens: number;
|
|
26
|
+
totalOutputTokens: number;
|
|
27
|
+
totalCalls: number;
|
|
28
|
+
byMethod: Record<string, MethodStats>;
|
|
29
|
+
byModel: Record<string, ModelStats>;
|
|
30
|
+
records: LLMCallRecord[];
|
|
31
|
+
}
|
|
32
|
+
export interface BudgetStatus {
|
|
33
|
+
globalBudget?: number;
|
|
34
|
+
totalCost: number;
|
|
35
|
+
remaining?: number;
|
|
36
|
+
isExceeded: boolean;
|
|
37
|
+
percentUsed?: number;
|
|
38
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"llm-usage.interface.js","sourceRoot":"","sources":["../../src/interfaces/llm-usage.interface.ts"],"names":[],"mappings":""}
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import { CallHandler, ExecutionContext, NestInterceptor } from '@nestjs/common';
|
|
2
|
+
import { Reflector } from '@nestjs/core';
|
|
3
|
+
import { Observable } from 'rxjs';
|
|
4
|
+
import { LLMBurnService } from './llm-burn.service';
|
|
5
|
+
export declare class LLMBurnInterceptor implements NestInterceptor {
|
|
6
|
+
private readonly llmBurnService;
|
|
7
|
+
private readonly reflector;
|
|
8
|
+
private readonly logger;
|
|
9
|
+
constructor(llmBurnService: LLMBurnService, reflector: Reflector);
|
|
10
|
+
intercept(context: ExecutionContext, next: CallHandler): Observable<unknown>;
|
|
11
|
+
private extractUsage;
|
|
12
|
+
private detectProvider;
|
|
13
|
+
}
|