@tokenwall/sdk 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/package.json +27 -0
- package/src/index.d.ts +32 -0
- package/src/index.js +121 -0
package/package.json
ADDED
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@tokenwall/sdk",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"description": "TokenWall SDK — enforce hard spend limits on LLM API calls in real time",
|
|
5
|
+
"main": "src/index.js",
|
|
6
|
+
"types": "src/index.d.ts",
|
|
7
|
+
"files": [
|
|
8
|
+
"src/"
|
|
9
|
+
],
|
|
10
|
+
"keywords": [
|
|
11
|
+
"tokenwall",
|
|
12
|
+
"llm",
|
|
13
|
+
"openai",
|
|
14
|
+
"anthropic",
|
|
15
|
+
"spend",
|
|
16
|
+
"cost",
|
|
17
|
+
"guardrail",
|
|
18
|
+
"ai"
|
|
19
|
+
],
|
|
20
|
+
"license": "UNLICENSED",
|
|
21
|
+
"publishConfig": {
|
|
22
|
+
"access": "public"
|
|
23
|
+
},
|
|
24
|
+
"engines": {
|
|
25
|
+
"node": ">=18"
|
|
26
|
+
}
|
|
27
|
+
}
|
package/src/index.d.ts
ADDED
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
export interface Wall {
|
|
2
|
+
wallUsd: number;
|
|
3
|
+
window: 'hourly' | 'daily' | 'weekly' | 'monthly';
|
|
4
|
+
scope?: 'org' | 'key' | 'user' | 'feature';
|
|
5
|
+
feature?: string;
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
export interface TokenWallOptions {
|
|
9
|
+
apiKey: string;
|
|
10
|
+
wallUsd?: number;
|
|
11
|
+
window?: 'hourly' | 'daily' | 'weekly' | 'monthly';
|
|
12
|
+
walls?: Wall[];
|
|
13
|
+
userId?: string;
|
|
14
|
+
feature?: string;
|
|
15
|
+
tags?: Record<string, string>;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
export interface TokenWallCallContext {
|
|
19
|
+
tokenwall?: {
|
|
20
|
+
userId?: string;
|
|
21
|
+
feature?: string;
|
|
22
|
+
tags?: Record<string, string>;
|
|
23
|
+
};
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
export class TokenWallBlockedError extends Error {
|
|
27
|
+
reason: 'wall_exceeded' | 'rate_limited';
|
|
28
|
+
wall: number;
|
|
29
|
+
spent: number;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
export function tokenwall<T>(client: T, options: TokenWallOptions): T;
|
package/src/index.js
ADDED
|
@@ -0,0 +1,121 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
const TOKENWALL_API = 'https://api.tokenwall.dev/v1';
|
|
4
|
+
|
|
5
|
+
class TokenWallBlockedError extends Error {
|
|
6
|
+
constructor({ reason, wall, spent }) {
|
|
7
|
+
super(`TokenWall blocked: ${reason} (spent $${spent} of $${wall} wall)`);
|
|
8
|
+
this.name = 'TokenWallBlockedError';
|
|
9
|
+
this.reason = reason;
|
|
10
|
+
this.wall = wall;
|
|
11
|
+
this.spent = spent;
|
|
12
|
+
}
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
function tokenwall(client, options = {}) {
|
|
16
|
+
const {
|
|
17
|
+
apiKey,
|
|
18
|
+
wallUsd,
|
|
19
|
+
window: timeWindow,
|
|
20
|
+
walls,
|
|
21
|
+
userId,
|
|
22
|
+
feature,
|
|
23
|
+
tags,
|
|
24
|
+
} = options;
|
|
25
|
+
|
|
26
|
+
if (!apiKey) throw new Error('TokenWall: apiKey is required');
|
|
27
|
+
|
|
28
|
+
// Proxy the client's chat.completions.create (OpenAI-compatible)
|
|
29
|
+
const originalCreate = client.chat?.completions?.create?.bind(client.chat.completions);
|
|
30
|
+
|
|
31
|
+
const guardrail = async (params, callOptions = {}) => {
|
|
32
|
+
const ctx = callOptions.tokenwall ?? {};
|
|
33
|
+
const effectiveUserId = ctx.userId ?? userId;
|
|
34
|
+
const effectiveFeature = ctx.feature ?? feature;
|
|
35
|
+
const effectiveTags = { ...tags, ...ctx.tags };
|
|
36
|
+
|
|
37
|
+
// Resolve walls for this call
|
|
38
|
+
const activeWalls = walls ?? (wallUsd != null ? [{ wallUsd, window: timeWindow ?? 'monthly' }] : []);
|
|
39
|
+
|
|
40
|
+
// Pre-flight check with TokenWall
|
|
41
|
+
let checkRes;
|
|
42
|
+
try {
|
|
43
|
+
checkRes = await fetch(`${TOKENWALL_API}/check`, {
|
|
44
|
+
method: 'POST',
|
|
45
|
+
headers: {
|
|
46
|
+
'Content-Type': 'application/json',
|
|
47
|
+
'Authorization': `Bearer ${apiKey}`,
|
|
48
|
+
},
|
|
49
|
+
body: JSON.stringify({
|
|
50
|
+
model: params.model,
|
|
51
|
+
walls: activeWalls,
|
|
52
|
+
userId: effectiveUserId,
|
|
53
|
+
feature: effectiveFeature,
|
|
54
|
+
tags: effectiveTags,
|
|
55
|
+
}),
|
|
56
|
+
});
|
|
57
|
+
} catch {
|
|
58
|
+
// If TokenWall is unreachable, fail open (let the call through)
|
|
59
|
+
return originalCreate(params, callOptions);
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
const check = await checkRes.json();
|
|
63
|
+
|
|
64
|
+
if (check.blocked) {
|
|
65
|
+
throw new TokenWallBlockedError({
|
|
66
|
+
reason: check.reason,
|
|
67
|
+
wall: check.wall,
|
|
68
|
+
spent: check.spent,
|
|
69
|
+
});
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
// Make the real call
|
|
73
|
+
const response = await originalCreate(params, callOptions);
|
|
74
|
+
|
|
75
|
+
// Post-call: report actual usage
|
|
76
|
+
const usage = response?.usage;
|
|
77
|
+
if (usage) {
|
|
78
|
+
fetch(`${TOKENWALL_API}/ingest`, {
|
|
79
|
+
method: 'POST',
|
|
80
|
+
headers: {
|
|
81
|
+
'Content-Type': 'application/json',
|
|
82
|
+
'Authorization': `Bearer ${apiKey}`,
|
|
83
|
+
},
|
|
84
|
+
body: JSON.stringify({
|
|
85
|
+
model: params.model,
|
|
86
|
+
promptTokens: usage.prompt_tokens,
|
|
87
|
+
completionTokens: usage.completion_tokens,
|
|
88
|
+
userId: effectiveUserId,
|
|
89
|
+
feature: effectiveFeature,
|
|
90
|
+
tags: effectiveTags,
|
|
91
|
+
}),
|
|
92
|
+
}).catch(() => {}); // fire-and-forget, never block the caller
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
return response;
|
|
96
|
+
};
|
|
97
|
+
|
|
98
|
+
// Return a proxy that wraps chat.completions.create
|
|
99
|
+
return new Proxy(client, {
|
|
100
|
+
get(target, prop) {
|
|
101
|
+
if (prop === 'chat') {
|
|
102
|
+
return new Proxy(target.chat, {
|
|
103
|
+
get(chatTarget, chatProp) {
|
|
104
|
+
if (chatProp === 'completions') {
|
|
105
|
+
return new Proxy(chatTarget.completions, {
|
|
106
|
+
get(compTarget, compProp) {
|
|
107
|
+
if (compProp === 'create') return guardrail;
|
|
108
|
+
return compTarget[compProp];
|
|
109
|
+
},
|
|
110
|
+
});
|
|
111
|
+
}
|
|
112
|
+
return chatTarget[chatProp];
|
|
113
|
+
},
|
|
114
|
+
});
|
|
115
|
+
}
|
|
116
|
+
return target[prop];
|
|
117
|
+
},
|
|
118
|
+
});
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
module.exports = { tokenwall, TokenWallBlockedError };
|