@spendgraph/sdk 0.5.0 → 0.6.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.
Files changed (46) hide show
  1. package/LICENSE +202 -0
  2. package/README.md +25 -80
  3. package/dist/ai.js +1 -25
  4. package/dist/client.js +1 -38
  5. package/dist/core/client/client.d.ts +2 -0
  6. package/dist/core/client/client.js +1 -132
  7. package/dist/core/client/errors.js +1 -17
  8. package/dist/core/client/index.js +1 -2
  9. package/dist/core/types.d.ts +4 -0
  10. package/dist/core/types.js +0 -1
  11. package/dist/index.d.ts +2 -1
  12. package/dist/index.js +1 -5
  13. package/dist/langchain.js +1 -69
  14. package/dist/resources/alerts.js +1 -9
  15. package/dist/resources/cli.js +1 -21
  16. package/dist/resources/credentials.js +1 -15
  17. package/dist/resources/events.js +1 -18
  18. package/dist/resources/index.d.ts +2 -2
  19. package/dist/resources/index.js +1 -13
  20. package/dist/resources/ingest.js +1 -27
  21. package/dist/resources/keys.js +1 -15
  22. package/dist/resources/playground.js +1 -9
  23. package/dist/resources/pricing.js +1 -36
  24. package/dist/resources/projects.d.ts +17 -3
  25. package/dist/resources/projects.js +1 -49
  26. package/dist/resources/prompts-admin.js +1 -21
  27. package/dist/resources/prompts.d.ts +1 -1
  28. package/dist/resources/prompts.js +1 -49
  29. package/dist/resources/stats.js +1 -21
  30. package/dist/resources/tools.d.ts +1 -1
  31. package/dist/resources/tools.js +1 -21
  32. package/dist/rollout/index.d.ts +1 -0
  33. package/dist/rollout/index.js +1 -1
  34. package/dist/rollout/rollout.d.ts +12 -0
  35. package/dist/rollout/rollout.js +1 -1
  36. package/dist/schema/index.d.ts +1 -1
  37. package/dist/schema/index.js +1 -2
  38. package/dist/schema/serialize/index.js +1 -1
  39. package/dist/schema/serialize/serialize.js +1 -31
  40. package/dist/schema/types/index.js +0 -1
  41. package/dist/schema/types/types.js +0 -1
  42. package/dist/schema/validate/index.js +1 -1
  43. package/dist/schema/validate/validate.js +1 -92
  44. package/dist/track/index.js +1 -1
  45. package/dist/track/track.js +1 -331
  46. package/package.json +5 -4
@@ -1,92 +1 @@
1
- export class FieldValidationError extends Error {
2
- errors;
3
- constructor(errors) {
4
- super(errors.map((e) => `${e.field}: ${e.message}`).join("; "));
5
- this.name = "FieldValidationError";
6
- this.errors = errors;
7
- }
8
- }
9
- export function isMissing(value) {
10
- return value === undefined || value === null;
11
- }
12
- export function toNumber(value) {
13
- return typeof value === "number" ? value : Number(String(value).trim());
14
- }
15
- export function toBoolean(value) {
16
- return typeof value === "boolean" ? value : String(value).toLowerCase() === "true";
17
- }
18
- function checkText(field, value) {
19
- const text = String(value);
20
- return field.maxLength !== undefined && text.length > field.maxLength
21
- ? `is ${text.length} characters, over the limit of ${field.maxLength}`
22
- : null;
23
- }
24
- function checkNumber(field, value) {
25
- const n = toNumber(value);
26
- if (!Number.isFinite(n))
27
- return `expects a number, got ${JSON.stringify(value)}`;
28
- if (field.min !== undefined && n < field.min)
29
- return `is ${n}, below the minimum of ${field.min}`;
30
- if (field.max !== undefined && n > field.max)
31
- return `is ${n}, above the maximum of ${field.max}`;
32
- return null;
33
- }
34
- function checkBoolean(value) {
35
- if (typeof value === "boolean")
36
- return null;
37
- const s = String(value).toLowerCase();
38
- return s === "true" || s === "false"
39
- ? null
40
- : `expects true or false, got ${JSON.stringify(value)}`;
41
- }
42
- function checkEnum(field, value) {
43
- const options = field.options ?? [];
44
- if (options.length === 0)
45
- return "is an enum with no options declared";
46
- return options.includes(String(value))
47
- ? null
48
- : `must be one of ${options.join(" | ")}, got ${JSON.stringify(value)}`;
49
- }
50
- function checkJson(value) {
51
- try {
52
- JSON.stringify(value);
53
- return null;
54
- }
55
- catch (err) {
56
- return `is not serialisable: ${err.message}`;
57
- }
58
- }
59
- function checkOne(field, value) {
60
- switch (field.type) {
61
- case "string":
62
- case "text":
63
- return checkText(field, value);
64
- case "number":
65
- return checkNumber(field, value);
66
- case "boolean":
67
- return checkBoolean(value);
68
- case "enum":
69
- return checkEnum(field, value);
70
- case "list":
71
- return Array.isArray(value) ? null : `expects an array, got ${typeof value}`;
72
- case "json":
73
- return checkJson(value);
74
- }
75
- }
76
- export function validateFields(values, spec) {
77
- const errors = [];
78
- for (const field of spec) {
79
- const value = values[field.name];
80
- if (isMissing(value)) {
81
- if (field.default !== undefined)
82
- continue;
83
- if (field.required)
84
- errors.push({ field: field.name, message: "is required" });
85
- continue;
86
- }
87
- const problem = checkOne(field, value);
88
- if (problem)
89
- errors.push({ field: field.name, message: problem });
90
- }
91
- return errors;
92
- }
1
+ class l extends Error{errors;constructor(e){super(e.map(t=>`${t.field}: ${t.message}`).join("; ")),this.name="FieldValidationError",this.errors=e}}function s(n){return n==null}function u(n){return typeof n=="number"?n:Number(String(n).trim())}function p(n){return typeof n=="boolean"?n:String(n).toLowerCase()==="true"}function c(n,e){const t=String(e);return n.maxLength!==void 0&&t.length>n.maxLength?`is ${t.length} characters, over the limit of ${n.maxLength}`:null}function a(n,e){const t=u(e);return Number.isFinite(t)?n.min!==void 0&&t<n.min?`is ${t}, below the minimum of ${n.min}`:n.max!==void 0&&t>n.max?`is ${t}, above the maximum of ${n.max}`:null:`expects a number, got ${JSON.stringify(e)}`}function m(n){if(typeof n=="boolean")return null;const e=String(n).toLowerCase();return e==="true"||e==="false"?null:`expects true or false, got ${JSON.stringify(n)}`}function f(n,e){const t=n.options??[];return t.length===0?"is an enum with no options declared":t.includes(String(e))?null:`must be one of ${t.join(" | ")}, got ${JSON.stringify(e)}`}function g(n){try{return JSON.stringify(n),null}catch(e){return`is not serialisable: ${e.message}`}}function h(n,e){switch(n.type){case"string":case"text":return c(n,e);case"number":return a(n,e);case"boolean":return m(e);case"enum":return f(n,e);case"list":return Array.isArray(e)?null:`expects an array, got ${typeof e}`;case"json":return g(e)}}function x(n,e){const t=[];for(const r of e){const o=n[r.name];if(s(o)){if(r.default!==void 0)continue;r.required&&t.push({field:r.name,message:"is required"});continue}const i=h(r,o);i&&t.push({field:r.name,message:i})}return t}export{l as FieldValidationError,s as isMissing,p as toBoolean,u as toNumber,x as validateFields};
@@ -1 +1 @@
1
- export { SpendGraph } from "./track.js";
1
+ import{SpendGraph as e}from"./track.js";export{e as SpendGraph};
@@ -1,331 +1 @@
1
- import { Client, SpendgraphError } from "../core/client/index.js";
2
- const MAX_RETRY_WAIT_MS = 2_000;
3
- const liveMeters = new Set();
4
- let exitHookInstalled = false;
5
- function registerForExitFlush(meter) {
6
- if (typeof process === "undefined" || typeof process.on !== "function")
7
- return;
8
- const ref = typeof WeakRef === "function"
9
- ? new WeakRef(meter)
10
- : { deref: () => meter };
11
- liveMeters.add(ref);
12
- if (exitHookInstalled)
13
- return;
14
- exitHookInstalled = true;
15
- process.on("beforeExit", () => {
16
- for (const r of liveMeters) {
17
- const m = r.deref();
18
- if (m)
19
- void m.flush();
20
- else
21
- liveMeters.delete(r);
22
- }
23
- });
24
- }
25
- export class SpendGraph {
26
- opts;
27
- queue = [];
28
- timer = null;
29
- warned = false;
30
- dropped = 0;
31
- unpricedSeen = new Set();
32
- interval;
33
- maxBatch;
34
- timerAt = 0;
35
- suspendWarned = false;
36
- noKeyWarned = false;
37
- streamUsageWarned = false;
38
- inFlight = null;
39
- constructor(opts) {
40
- this.opts = opts;
41
- this.interval = opts.flushIntervalMs ?? 5000;
42
- this.maxBatch = opts.maxBatch ?? 20;
43
- registerForExitFlush(this);
44
- }
45
- clientFor(apiKey) {
46
- return new Client({ apiKey, baseUrl: this.opts.baseUrl, attempts: 1 });
47
- }
48
- track(event) {
49
- try {
50
- if (!this.opts.apiKey) {
51
- this.warnNoKey();
52
- return;
53
- }
54
- this.detectSuspendedRuntime();
55
- this.queue.push(event);
56
- if (this.queue.length >= this.maxBatch) {
57
- void this.flush();
58
- }
59
- else if (!this.timer) {
60
- this.timer = setTimeout(() => void this.flush(), this.interval);
61
- this.timerAt = Date.now();
62
- this.timer.unref?.();
63
- }
64
- }
65
- catch {
66
- }
67
- }
68
- warnNoKey() {
69
- if (this.noKeyWarned)
70
- return;
71
- this.noKeyWarned = true;
72
- console.warn("spendgraph: no apiKey set, so track() is recording nothing. Pass apiKey " +
73
- "(usually from SPENDGRAPH_API_KEY) to start tracking, or ignore this if " +
74
- "tracking is meant to be off here.");
75
- }
76
- detectSuspendedRuntime() {
77
- if (!this.timer)
78
- return;
79
- if (Date.now() - this.timerAt <= this.interval * 2)
80
- return;
81
- if (!this.suspendWarned) {
82
- this.suspendWarned = true;
83
- console.warn("spendgraph: a scheduled flush never ran — this runtime suspends between " +
84
- "invocations, so buffered events are lost. Await meter.flush() before " +
85
- "your handler returns.");
86
- }
87
- void this.flush();
88
- }
89
- async flush() {
90
- const prev = this.inFlight;
91
- const mine = this.drain();
92
- const run = Promise.allSettled([prev, mine]).then(() => undefined);
93
- this.inFlight = run;
94
- try {
95
- await run;
96
- }
97
- finally {
98
- if (this.inFlight === run)
99
- this.inFlight = null;
100
- }
101
- }
102
- async drain() {
103
- if (this.timer) {
104
- clearTimeout(this.timer);
105
- this.timer = null;
106
- }
107
- const apiKey = this.opts.apiKey;
108
- if (this.queue.length === 0 || !apiKey)
109
- return;
110
- const events = this.queue.splice(0, this.queue.length);
111
- for (let i = 0; i < events.length; i += 100) {
112
- await this.send(events.slice(i, i + 100), apiKey);
113
- }
114
- }
115
- wrap(client) {
116
- return this.proxy(client);
117
- }
118
- proxy(target) {
119
- const cache = new Map();
120
- return new Proxy(target, {
121
- get: (obj, prop) => {
122
- const value = Reflect.get(obj, prop);
123
- if (value === null || (typeof value !== "function" && typeof value !== "object")) {
124
- return value;
125
- }
126
- const hit = cache.get(prop);
127
- if (hit && hit.from === value)
128
- return hit.out;
129
- const out = typeof value === "function"
130
- ? (...args) => this.observeResult(value.apply(obj, args), args)
131
- : this.proxy(value);
132
- cache.set(prop, { from: value, out });
133
- return out;
134
- },
135
- });
136
- }
137
- observeResult(result, args) {
138
- try {
139
- const helper = result;
140
- const final = typeof helper?.finalMessage === "function"
141
- ? helper.finalMessage()
142
- : typeof helper?.finalChatCompletion === "function"
143
- ? helper.finalChatCompletion()
144
- : null;
145
- if (final instanceof Promise) {
146
- final.then((m) => this.trackFromResponse(m), () => { });
147
- return result;
148
- }
149
- if (result instanceof Promise) {
150
- const wantsStream = !!args[0]?.stream;
151
- if (wantsStream) {
152
- return result.then((v) => this.interceptStream(v));
153
- }
154
- result.then((v) => this.trackFromResponse(v), () => { });
155
- }
156
- }
157
- catch {
158
- }
159
- return result;
160
- }
161
- interceptStream(v) {
162
- try {
163
- const s = v;
164
- if (s && typeof s.tee === "function" && typeof s[Symbol.asyncIterator] === "function") {
165
- const [mine, theirs] = s.tee();
166
- void this.consumeStream(mine);
167
- return theirs;
168
- }
169
- this.trackFromResponse(v);
170
- }
171
- catch {
172
- }
173
- return v;
174
- }
175
- async consumeStream(iter) {
176
- try {
177
- let model;
178
- let inputTokens;
179
- let outputTokens;
180
- let cacheReadTokens = 0;
181
- let cacheWriteTokens = 0;
182
- let citationTokens;
183
- let reasoningTokens;
184
- let sawChunk = false;
185
- for await (const raw of iter) {
186
- sawChunk = true;
187
- const ev = raw;
188
- if (ev?.type === "message_start" && ev.message) {
189
- model = ev.message.model ?? model;
190
- const u = ev.message.usage ?? {};
191
- inputTokens = u.input_tokens ?? inputTokens;
192
- cacheReadTokens = u.cache_read_input_tokens ?? 0;
193
- cacheWriteTokens = u.cache_creation_input_tokens ?? 0;
194
- }
195
- else if (ev?.type === "message_delta" && ev.usage) {
196
- outputTokens = ev.usage.output_tokens ?? outputTokens;
197
- }
198
- else if (ev?.object === "chat.completion.chunk") {
199
- model = ev.model ?? model;
200
- if (ev.usage) {
201
- const u = ev.usage;
202
- const cached = u.prompt_tokens_details?.cached_tokens ?? 0;
203
- inputTokens = Math.max(0, (u.prompt_tokens ?? 0) - cached);
204
- cacheReadTokens = cached;
205
- outputTokens = u.completion_tokens;
206
- citationTokens = u.citation_tokens;
207
- reasoningTokens = u.reasoning_tokens;
208
- }
209
- }
210
- }
211
- if (model && typeof inputTokens === "number" && typeof outputTokens === "number") {
212
- this.track({
213
- model,
214
- inputTokens,
215
- outputTokens,
216
- cacheReadTokens,
217
- cacheWriteTokens,
218
- citationTokens,
219
- reasoningTokens,
220
- });
221
- }
222
- else if (sawChunk) {
223
- this.warnStreamNoUsage();
224
- }
225
- }
226
- catch {
227
- }
228
- }
229
- warnStreamNoUsage() {
230
- if (this.streamUsageWarned)
231
- return;
232
- this.streamUsageWarned = true;
233
- console.warn("spendgraph: a streamed response carried no token usage, so that call " +
234
- "was not tracked. For OpenAI raw streams pass " +
235
- "stream_options: { include_usage: true }. Further untracked streams " +
236
- "are not logged.");
237
- }
238
- trackFromResponse(res) {
239
- try {
240
- if (!res || typeof res !== "object")
241
- return;
242
- const r = res;
243
- if (!r.usage || !r.model)
244
- return;
245
- const u = r.usage;
246
- if (typeof u.input_tokens === "number" && typeof u.output_tokens === "number") {
247
- this.track({
248
- model: r.model,
249
- inputTokens: u.input_tokens,
250
- outputTokens: u.output_tokens,
251
- cacheReadTokens: u.cache_read_input_tokens ?? 0,
252
- cacheWriteTokens: u.cache_creation_input_tokens ?? 0,
253
- });
254
- return;
255
- }
256
- if (typeof u.prompt_tokens === "number" && typeof u.completion_tokens === "number") {
257
- const cached = u.prompt_tokens_details?.cached_tokens ?? 0;
258
- this.track({
259
- model: r.model,
260
- inputTokens: Math.max(0, u.prompt_tokens - cached),
261
- outputTokens: u.completion_tokens,
262
- cacheReadTokens: cached,
263
- citationTokens: u.citation_tokens,
264
- reasoningTokens: u.reasoning_tokens,
265
- });
266
- }
267
- }
268
- catch {
269
- }
270
- }
271
- async send(events, apiKey, attempt = 0) {
272
- let body;
273
- try {
274
- body = await this.clientFor(apiKey).post("/api/v1/ingest", { events });
275
- }
276
- catch (err) {
277
- return this.onSendFailed(err, events, apiKey, attempt);
278
- }
279
- this.warned = false;
280
- this.dropped = 0;
281
- this.reportUnpriced(body);
282
- }
283
- async onSendFailed(err, events, apiKey, attempt) {
284
- const failure = err instanceof SpendgraphError ? err : null;
285
- if (!failure || failure.status === 0) {
286
- if (attempt === 0)
287
- return this.send(events, apiKey, 1);
288
- this.reportDropped(events.length, `unreachable (${String(err)})`);
289
- return;
290
- }
291
- if (failure.status >= 500 && attempt === 0) {
292
- return this.send(events, apiKey, 1);
293
- }
294
- if (failure.status === 429 && attempt === 0) {
295
- const waitMs = failure.retryAfterMs;
296
- if (waitMs !== null && waitMs <= MAX_RETRY_WAIT_MS) {
297
- await new Promise((r) => setTimeout(r, waitMs));
298
- return this.send(events, apiKey, 1);
299
- }
300
- this.reportDropped(events.length, waitMs === null
301
- ? "rate limited by ingest"
302
- : `rate limited by ingest, clear in ${Math.ceil(waitMs / 1000)}s`);
303
- return;
304
- }
305
- this.reportDropped(events.length, `ingest returned ${failure.status}`);
306
- }
307
- reportUnpriced(body) {
308
- {
309
- const parsed = body;
310
- if (!Array.isArray(parsed?.unpricedModels))
311
- return;
312
- const fresh = parsed.unpricedModels
313
- .filter((m) => typeof m === "string")
314
- .filter((m) => !this.unpricedSeen.has(m));
315
- if (fresh.length === 0)
316
- return;
317
- for (const m of fresh)
318
- this.unpricedSeen.add(m);
319
- console.warn(`spendgraph: no price for ${fresh.map((m) => `"${m}"`).join(", ")}. ` +
320
- `These events are recorded but cost $0 until the model is in the catalog.`);
321
- }
322
- }
323
- reportDropped(count, reason) {
324
- this.dropped += count;
325
- if (this.warned)
326
- return;
327
- this.warned = true;
328
- console.warn(`spendgraph: ${reason}; dropped ${this.dropped} event(s). ` +
329
- `Further drops are counted but not logged until a flush succeeds.`);
330
- }
331
- }
1
+ import{Client as g,SpendgraphError as k}from"../core/client/index.js";const y=2e3,l=new Set;let f=!1;function _(h){if(typeof process>"u"||typeof process.on!="function")return;const t=typeof WeakRef=="function"?new WeakRef(h):{deref:()=>h};l.add(t),!f&&(f=!0,process.on("beforeExit",()=>{for(const n of l){const e=n.deref();e?e.flush():l.delete(n)}}))}class b{opts;queue=[];timer=null;warned=!1;dropped=0;unpricedSeen=new Set;interval;maxBatch;timerAt=0;suspendWarned=!1;noKeyWarned=!1;streamUsageWarned=!1;inFlight=null;constructor(t){this.opts=t,this.interval=t.flushIntervalMs??5e3,this.maxBatch=t.maxBatch??20,_(this)}clientFor(t){return new g({apiKey:t,baseUrl:this.opts.baseUrl,attempts:1})}track(t){try{if(!this.opts.apiKey){this.warnNoKey();return}this.detectSuspendedRuntime(),this.queue.push(t),this.queue.length>=this.maxBatch?this.flush():this.timer||(this.timer=setTimeout(()=>void this.flush(),this.interval),this.timerAt=Date.now(),this.timer.unref?.())}catch{}}warnNoKey(){this.noKeyWarned||(this.noKeyWarned=!0,console.warn("spendgraph: no apiKey set, so track() is recording nothing. Pass apiKey (usually from SPENDGRAPH_API_KEY) to start tracking, or ignore this if tracking is meant to be off here."))}detectSuspendedRuntime(){this.timer&&(Date.now()-this.timerAt<=this.interval*2||(this.suspendWarned||(this.suspendWarned=!0,console.warn("spendgraph: a scheduled flush never ran \u2014 this runtime suspends between invocations, so buffered events are lost. Await meter.flush() before your handler returns.")),this.flush()))}async flush(){const t=this.inFlight,n=this.drain(),e=Promise.allSettled([t,n]).then(()=>{});this.inFlight=e;try{await e}finally{this.inFlight===e&&(this.inFlight=null)}}async drain(){this.timer&&(clearTimeout(this.timer),this.timer=null);const t=this.opts.apiKey;if(this.queue.length===0||!t)return;const n=this.queue.splice(0,this.queue.length);for(let e=0;e<n.length;e+=100)await this.send(n.slice(e,e+100),t)}wrap(t){return this.proxy(t)}proxy(t){const n=new Map;return new Proxy(t,{get:(e,s)=>{const r=Reflect.get(e,s);if(r===null||typeof r!="function"&&typeof r!="object")return r;const o=n.get(s);if(o&&o.from===r)return o.out;const c=typeof r=="function"?(...u)=>this.observeResult(r.apply(e,u),u):this.proxy(r);return n.set(s,{from:r,out:c}),c}})}observeResult(t,n){try{const e=t,s=typeof e?.finalMessage=="function"?e.finalMessage():typeof e?.finalChatCompletion=="function"?e.finalChatCompletion():null;if(s instanceof Promise)return s.then(r=>this.trackFromResponse(r),()=>{}),t;if(t instanceof Promise){if(!!n[0]?.stream)return t.then(o=>this.interceptStream(o));t.then(o=>this.trackFromResponse(o),()=>{})}}catch{}return t}interceptStream(t){try{const n=t;if(n&&typeof n.tee=="function"&&typeof n[Symbol.asyncIterator]=="function"){const[e,s]=n.tee();return this.consumeStream(e),s}this.trackFromResponse(t)}catch{}return t}async consumeStream(t){try{let n,e,s,r=0,o=0,c,u,p=!1;for await(const m of t){p=!0;const i=m;if(i?.type==="message_start"&&i.message){n=i.message.model??n;const a=i.message.usage??{};e=a.input_tokens??e,r=a.cache_read_input_tokens??0,o=a.cache_creation_input_tokens??0}else if(i?.type==="message_delta"&&i.usage)s=i.usage.output_tokens??s;else if(i?.object==="chat.completion.chunk"&&(n=i.model??n,i.usage)){const a=i.usage,d=a.prompt_tokens_details?.cached_tokens??0;e=Math.max(0,(a.prompt_tokens??0)-d),r=d,s=a.completion_tokens,c=a.citation_tokens,u=a.reasoning_tokens}}n&&typeof e=="number"&&typeof s=="number"?this.track({model:n,inputTokens:e,outputTokens:s,cacheReadTokens:r,cacheWriteTokens:o,citationTokens:c,reasoningTokens:u}):p&&this.warnStreamNoUsage()}catch{}}warnStreamNoUsage(){this.streamUsageWarned||(this.streamUsageWarned=!0,console.warn("spendgraph: a streamed response carried no token usage, so that call was not tracked. For OpenAI raw streams pass stream_options: { include_usage: true }. Further untracked streams are not logged."))}trackFromResponse(t){try{if(!t||typeof t!="object")return;const n=t;if(!n.usage||!n.model)return;const e=n.usage;if(typeof e.input_tokens=="number"&&typeof e.output_tokens=="number"){this.track({model:n.model,inputTokens:e.input_tokens,outputTokens:e.output_tokens,cacheReadTokens:e.cache_read_input_tokens??0,cacheWriteTokens:e.cache_creation_input_tokens??0});return}if(typeof e.prompt_tokens=="number"&&typeof e.completion_tokens=="number"){const s=e.prompt_tokens_details?.cached_tokens??0;this.track({model:n.model,inputTokens:Math.max(0,e.prompt_tokens-s),outputTokens:e.completion_tokens,cacheReadTokens:s,citationTokens:e.citation_tokens,reasoningTokens:e.reasoning_tokens})}}catch{}}async send(t,n,e=0){let s;try{s=await this.clientFor(n).post("/api/v1/ingest",{events:t})}catch(r){return this.onSendFailed(r,t,n,e)}this.warned=!1,this.dropped=0,this.reportUnpriced(s)}async onSendFailed(t,n,e,s){const r=t instanceof k?t:null;if(!r||r.status===0){if(s===0)return this.send(n,e,1);this.reportDropped(n.length,`unreachable (${String(t)})`);return}if(r.status>=500&&s===0)return this.send(n,e,1);if(r.status===429&&s===0){const o=r.retryAfterMs;if(o!==null&&o<=y)return await new Promise(c=>setTimeout(c,o)),this.send(n,e,1);this.reportDropped(n.length,o===null?"rate limited by ingest":`rate limited by ingest, clear in ${Math.ceil(o/1e3)}s`);return}this.reportDropped(n.length,`ingest returned ${r.status}`)}reportUnpriced(t){{const n=t;if(!Array.isArray(n?.unpricedModels))return;const e=n.unpricedModels.filter(s=>typeof s=="string").filter(s=>!this.unpricedSeen.has(s));if(e.length===0)return;for(const s of e)this.unpricedSeen.add(s);console.warn(`spendgraph: no price for ${e.map(s=>`"${s}"`).join(", ")}. These events are recorded but cost $0 until the model is in the catalog.`)}}reportDropped(t,n){this.dropped+=t,!this.warned&&(this.warned=!0,console.warn(`spendgraph: ${n}; dropped ${this.dropped} event(s). Further drops are counted but not logged until a flush succeeds.`))}}export{b as SpendGraph};
package/package.json CHANGED
@@ -1,8 +1,8 @@
1
1
  {
2
2
  "name": "@spendgraph/sdk",
3
- "version": "0.5.0",
3
+ "version": "0.6.0",
4
4
  "description": "Track LLM input/output tokens and cost. Three functions, zero dependencies, fail-open.",
5
- "license": "MIT",
5
+ "license": "Apache-2.0",
6
6
  "repository": {
7
7
  "type": "git",
8
8
  "url": "git+https://github.com/fnLog0/spendgraph.git",
@@ -36,14 +36,15 @@
36
36
  "./ai": {
37
37
  "types": "./dist/ai.d.ts",
38
38
  "import": "./dist/ai.js"
39
- }
39
+ },
40
+ "./package.json": "./package.json"
40
41
  },
41
42
  "files": [
42
43
  "dist",
43
44
  "README.md"
44
45
  ],
45
46
  "scripts": {
46
- "build": "tsc -p tsconfig.json --emitDeclarationOnly && tsc -p tsconfig.json --declaration false --removeComments",
47
+ "build": "rm -rf dist && tsc -p tsconfig.json --emitDeclarationOnly && tsc -p tsconfig.json --declaration false --removeComments && node ../../scripts/minify.mjs dist",
47
48
  "test": "vitest run"
48
49
  },
49
50
  "devDependencies": {