@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 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 };