@loopers/client 0.4.3

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 ADDED
@@ -0,0 +1,79 @@
1
+ # Loopers TypeScript SDK (`@loopers/client`)
2
+
3
+ The `@loopers/client` package provides a drop-in wrapper around official OpenAI and Anthropic SDK clients to make integration with the Loopers cost firewall seamless in Node.js and browser environments.
4
+
5
+ ## Installation
6
+
7
+ ```bash
8
+ npm install @loopers/client
9
+ ```
10
+
11
+ Additionally, install the official provider package you plan to use:
12
+
13
+ ```bash
14
+ npm install openai
15
+ # or
16
+ npm install @anthropic-ai/sdk
17
+ ```
18
+
19
+ ## Features
20
+
21
+ - **Automatic Headers Injection**: Automatically handles injection of Loopers proxy keys (`Authorization`), upstream provider keys (`X-Loopers-Provider-Key`), and session budget limits.
22
+ - **Custom Attributes**: Intercepts response parsing and attaches cost metadata directly to the returned objects.
23
+
24
+ ## Usage
25
+
26
+ ### OpenAI Integration
27
+
28
+ Replace `OpenAI` with `LoopersOpenAI`:
29
+
30
+ ```typescript
31
+ import { LoopersOpenAI } from '@loopers/client';
32
+
33
+ // Initialize client
34
+ const client = new LoopersOpenAI({
35
+ loopersUrl: 'http://localhost:8080',
36
+ loopersKey: 'lp-xxx', // Loopers proxy key
37
+ providerKey: 'sk-proj-xxx', // Upstream OpenAI key
38
+ sessionId: 'agent-run-123', // Optional: track steps and budget for an agent session
39
+ sessionBudget: 2.50, // Optional: limit session to $2.50
40
+ maxSteps: 20 // Optional: limit session to 20 steps
41
+ });
42
+
43
+ // Call completions exactly like the official client
44
+ const response = await client.chat.completions.create({
45
+ model: 'gpt-4o',
46
+ messages: [{ role: 'user', content: 'Hello!' }],
47
+ });
48
+
49
+ // Inspect budget/cost metadata attached to response
50
+ console.log(`Request Cost: $${(response as any).loopers_cost} USD`);
51
+ console.log(`Estimated Cost: $${(response as any).loopers_cost_estimated} USD`);
52
+ console.log(`Session Spend: $${(response as any).loopers_session_spend} USD`);
53
+ console.log(`Session Steps: ${(response as any).loopers_session_steps}`);
54
+ console.log(`Session Remaining: $${(response as any).loopers_session_remaining} USD`);
55
+ ```
56
+
57
+ ### Anthropic Integration
58
+
59
+ ```typescript
60
+ import { LoopersAnthropic } from '@loopers/client';
61
+
62
+ const client = new LoopersAnthropic({
63
+ loopersUrl: 'http://localhost:8080',
64
+ loopersKey: 'lp-xxx',
65
+ providerKey: 'sk-ant-xxx'
66
+ });
67
+
68
+ const response = await client.messages.create({
69
+ model: 'claude-3-5-sonnet-latest',
70
+ max_tokens: 1000,
71
+ messages: [{ role: 'user', content: 'Hello!' }]
72
+ });
73
+
74
+ console.log(`Request Cost: $${(response as any).loopers_cost} USD`);
75
+ ```
76
+
77
+ ## License
78
+
79
+ MIT
@@ -0,0 +1,20 @@
1
+ import OpenAI from 'openai';
2
+ import Anthropic from '@anthropic-ai/sdk';
3
+ export interface LoopersClientOptions {
4
+ loopersUrl: string;
5
+ loopersKey: string;
6
+ providerKey?: string;
7
+ sessionId?: string;
8
+ sessionBudget?: number;
9
+ maxSteps?: number;
10
+ }
11
+ export declare class LoopersOpenAI extends OpenAI {
12
+ constructor(options: LoopersClientOptions & {
13
+ [key: string]: any;
14
+ });
15
+ }
16
+ export declare class LoopersAnthropic extends Anthropic {
17
+ constructor(options: LoopersClientOptions & {
18
+ [key: string]: any;
19
+ });
20
+ }
package/dist/client.js ADDED
@@ -0,0 +1,106 @@
1
+ "use strict";
2
+ var __importDefault = (this && this.__importDefault) || function (mod) {
3
+ return (mod && mod.__esModule) ? mod : { "default": mod };
4
+ };
5
+ Object.defineProperty(exports, "__esModule", { value: true });
6
+ exports.LoopersAnthropic = exports.LoopersOpenAI = void 0;
7
+ const openai_1 = __importDefault(require("openai"));
8
+ const sdk_1 = __importDefault(require("@anthropic-ai/sdk"));
9
+ function createLoopersFetch(loopersKey, providerKey, sessionId, sessionBudget, maxSteps, customFetch) {
10
+ const originalFetch = customFetch || (typeof fetch !== 'undefined' ? fetch : undefined);
11
+ if (!originalFetch) {
12
+ throw new Error('A global fetch function is not available. Please pass a custom fetch implementation.');
13
+ }
14
+ return async (input, init) => {
15
+ // In JS/TS, headers can be in different structures
16
+ const headers = new Headers(init?.headers || {});
17
+ // Set Loopers headers
18
+ headers.set('Authorization', `Bearer ${loopersKey}`);
19
+ if (providerKey) {
20
+ headers.set('X-Loopers-Provider-Key', providerKey);
21
+ }
22
+ if (sessionId) {
23
+ headers.set('X-Loopers-Session-ID', sessionId);
24
+ }
25
+ if (sessionBudget !== undefined) {
26
+ headers.set('X-Loopers-Session-Budget', String(sessionBudget));
27
+ }
28
+ if (maxSteps !== undefined) {
29
+ headers.set('X-Loopers-Session-Max-Steps', String(maxSteps));
30
+ }
31
+ const modifiedInit = {
32
+ ...init,
33
+ headers,
34
+ };
35
+ const res = await originalFetch(input, modifiedInit);
36
+ // Intercept response.json() to attach loopers properties to the returned parsed object
37
+ const originalJson = res.json;
38
+ res.json = async () => {
39
+ const data = await originalJson.call(res);
40
+ if (data && typeof data === 'object') {
41
+ const costVal = res.headers.get('X-Loopers-Request-Cost');
42
+ const estCostVal = res.headers.get('X-Loopers-Request-Cost-Estimated');
43
+ const spendVal = res.headers.get('X-Loopers-Session-Spend');
44
+ const stepsVal = res.headers.get('X-Loopers-Session-Steps');
45
+ const remainingVal = res.headers.get('X-Loopers-Session-Remaining');
46
+ Object.defineProperties(data, {
47
+ loopers_cost: {
48
+ value: costVal ? Number(costVal) : null,
49
+ writable: true,
50
+ enumerable: true,
51
+ },
52
+ loopers_cost_estimated: {
53
+ value: estCostVal ? Number(estCostVal) : null,
54
+ writable: true,
55
+ enumerable: true,
56
+ },
57
+ loopers_session_spend: {
58
+ value: spendVal ? Number(spendVal) : null,
59
+ writable: true,
60
+ enumerable: true,
61
+ },
62
+ loopers_session_steps: {
63
+ value: stepsVal ? Number(stepsVal) : null,
64
+ writable: true,
65
+ enumerable: true,
66
+ },
67
+ loopers_session_remaining: {
68
+ value: remainingVal ? Number(remainingVal) : null,
69
+ writable: true,
70
+ enumerable: true,
71
+ },
72
+ });
73
+ }
74
+ return data;
75
+ };
76
+ return res;
77
+ };
78
+ }
79
+ class LoopersOpenAI extends openai_1.default {
80
+ constructor(options) {
81
+ const { loopersUrl, loopersKey, providerKey, sessionId, sessionBudget, maxSteps, ...openaiOptions } = options;
82
+ const baseFetch = openaiOptions.fetch;
83
+ const loopersFetch = createLoopersFetch(loopersKey, providerKey, sessionId, sessionBudget, maxSteps, baseFetch);
84
+ super({
85
+ ...openaiOptions,
86
+ baseURL: `${loopersUrl.replace(/\/$/, '')}/openai/v1`,
87
+ apiKey: loopersKey,
88
+ fetch: loopersFetch,
89
+ });
90
+ }
91
+ }
92
+ exports.LoopersOpenAI = LoopersOpenAI;
93
+ class LoopersAnthropic extends sdk_1.default {
94
+ constructor(options) {
95
+ const { loopersUrl, loopersKey, providerKey, sessionId, sessionBudget, maxSteps, ...anthropicOptions } = options;
96
+ const baseFetch = anthropicOptions.fetch;
97
+ const loopersFetch = createLoopersFetch(loopersKey, providerKey, sessionId, sessionBudget, maxSteps, baseFetch);
98
+ super({
99
+ ...anthropicOptions,
100
+ baseURL: `${loopersUrl.replace(/\/$/, '')}/anthropic`,
101
+ authToken: loopersKey,
102
+ fetch: loopersFetch,
103
+ });
104
+ }
105
+ }
106
+ exports.LoopersAnthropic = LoopersAnthropic;
@@ -0,0 +1 @@
1
+ export { LoopersOpenAI, LoopersAnthropic, LoopersClientOptions } from './client';
package/dist/index.js ADDED
@@ -0,0 +1,6 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.LoopersAnthropic = exports.LoopersOpenAI = void 0;
4
+ var client_1 = require("./client");
5
+ Object.defineProperty(exports, "LoopersOpenAI", { enumerable: true, get: function () { return client_1.LoopersOpenAI; } });
6
+ Object.defineProperty(exports, "LoopersAnthropic", { enumerable: true, get: function () { return client_1.LoopersAnthropic; } });
package/package.json ADDED
@@ -0,0 +1,38 @@
1
+ {
2
+ "name": "@loopers/client",
3
+ "version": "0.4.3",
4
+ "description": "A premium TypeScript client wrapper for the Loopers AI budget & rate-limit proxy.",
5
+ "main": "dist/index.js",
6
+ "types": "dist/index.d.ts",
7
+ "scripts": {
8
+ "build": "tsc"
9
+ },
10
+ "keywords": [
11
+ "loopers",
12
+ "budget",
13
+ "firewall",
14
+ "openai",
15
+ "anthropic",
16
+ "proxy"
17
+ ],
18
+ "author": "Loopers OSS Contributors",
19
+ "license": "MIT",
20
+ "peerDependencies": {
21
+ "openai": "^4.0.0",
22
+ "@anthropic-ai/sdk": "^0.30.0"
23
+ },
24
+ "peerDependenciesMeta": {
25
+ "openai": {
26
+ "optional": true
27
+ },
28
+ "@anthropic-ai/sdk": {
29
+ "optional": true
30
+ }
31
+ },
32
+ "dependencies": {},
33
+ "devDependencies": {
34
+ "typescript": "^5.0.0",
35
+ "openai": "^4.0.0",
36
+ "@anthropic-ai/sdk": "^0.30.0"
37
+ }
38
+ }
package/src/client.ts ADDED
@@ -0,0 +1,166 @@
1
+ import OpenAI from 'openai';
2
+ import Anthropic from '@anthropic-ai/sdk';
3
+
4
+ export interface LoopersClientOptions {
5
+ loopersUrl: string;
6
+ loopersKey: string;
7
+ providerKey?: string;
8
+ sessionId?: string;
9
+ sessionBudget?: number;
10
+ maxSteps?: number;
11
+ }
12
+
13
+ function createLoopersFetch(
14
+ loopersKey: string,
15
+ providerKey?: string,
16
+ sessionId?: string,
17
+ sessionBudget?: number,
18
+ maxSteps?: number,
19
+ customFetch?: typeof fetch
20
+ ) {
21
+ const originalFetch = customFetch || (typeof fetch !== 'undefined' ? fetch : undefined);
22
+ if (!originalFetch) {
23
+ throw new Error('A global fetch function is not available. Please pass a custom fetch implementation.');
24
+ }
25
+
26
+ return async (input: RequestInfo | URL, init?: RequestInit): Promise<Response> => {
27
+ // In JS/TS, headers can be in different structures
28
+ const headers = new Headers(init?.headers || {});
29
+
30
+ // Set Loopers headers
31
+ headers.set('Authorization', `Bearer ${loopersKey}`);
32
+ if (providerKey) {
33
+ headers.set('X-Loopers-Provider-Key', providerKey);
34
+ }
35
+ if (sessionId) {
36
+ headers.set('X-Loopers-Session-ID', sessionId);
37
+ }
38
+ if (sessionBudget !== undefined) {
39
+ headers.set('X-Loopers-Session-Budget', String(sessionBudget));
40
+ }
41
+ if (maxSteps !== undefined) {
42
+ headers.set('X-Loopers-Session-Max-Steps', String(maxSteps));
43
+ }
44
+
45
+ const modifiedInit = {
46
+ ...init,
47
+ headers,
48
+ };
49
+
50
+ const res = await originalFetch(input, modifiedInit);
51
+
52
+ // Intercept response.json() to attach loopers properties to the returned parsed object
53
+ const originalJson = res.json;
54
+ res.json = async () => {
55
+ const data = await originalJson.call(res);
56
+ if (data && typeof data === 'object') {
57
+ const costVal = res.headers.get('X-Loopers-Request-Cost');
58
+ const estCostVal = res.headers.get('X-Loopers-Request-Cost-Estimated');
59
+ const spendVal = res.headers.get('X-Loopers-Session-Spend');
60
+ const stepsVal = res.headers.get('X-Loopers-Session-Steps');
61
+ const remainingVal = res.headers.get('X-Loopers-Session-Remaining');
62
+
63
+ Object.defineProperties(data, {
64
+ loopers_cost: {
65
+ value: costVal ? Number(costVal) : null,
66
+ writable: true,
67
+ enumerable: true,
68
+ },
69
+ loopers_cost_estimated: {
70
+ value: estCostVal ? Number(estCostVal) : null,
71
+ writable: true,
72
+ enumerable: true,
73
+ },
74
+ loopers_session_spend: {
75
+ value: spendVal ? Number(spendVal) : null,
76
+ writable: true,
77
+ enumerable: true,
78
+ },
79
+ loopers_session_steps: {
80
+ value: stepsVal ? Number(stepsVal) : null,
81
+ writable: true,
82
+ enumerable: true,
83
+ },
84
+ loopers_session_remaining: {
85
+ value: remainingVal ? Number(remainingVal) : null,
86
+ writable: true,
87
+ enumerable: true,
88
+ },
89
+ });
90
+ }
91
+ return data;
92
+ };
93
+
94
+ return res;
95
+ };
96
+ }
97
+
98
+ export class LoopersOpenAI extends OpenAI {
99
+ constructor(
100
+ options: LoopersClientOptions & {
101
+ [key: string]: any;
102
+ }
103
+ ) {
104
+ const {
105
+ loopersUrl,
106
+ loopersKey,
107
+ providerKey,
108
+ sessionId,
109
+ sessionBudget,
110
+ maxSteps,
111
+ ...openaiOptions
112
+ } = options;
113
+
114
+ const baseFetch = openaiOptions.fetch;
115
+ const loopersFetch = createLoopersFetch(
116
+ loopersKey,
117
+ providerKey,
118
+ sessionId,
119
+ sessionBudget,
120
+ maxSteps,
121
+ baseFetch
122
+ );
123
+
124
+ super({
125
+ ...openaiOptions,
126
+ baseURL: `${loopersUrl.replace(/\/$/, '')}/openai/v1`,
127
+ apiKey: loopersKey,
128
+ fetch: loopersFetch,
129
+ });
130
+ }
131
+ }
132
+
133
+ export class LoopersAnthropic extends Anthropic {
134
+ constructor(
135
+ options: LoopersClientOptions & {
136
+ [key: string]: any;
137
+ }
138
+ ) {
139
+ const {
140
+ loopersUrl,
141
+ loopersKey,
142
+ providerKey,
143
+ sessionId,
144
+ sessionBudget,
145
+ maxSteps,
146
+ ...anthropicOptions
147
+ } = options;
148
+
149
+ const baseFetch = anthropicOptions.fetch;
150
+ const loopersFetch = createLoopersFetch(
151
+ loopersKey,
152
+ providerKey,
153
+ sessionId,
154
+ sessionBudget,
155
+ maxSteps,
156
+ baseFetch
157
+ );
158
+
159
+ super({
160
+ ...anthropicOptions,
161
+ baseURL: `${loopersUrl.replace(/\/$/, '')}/anthropic`,
162
+ authToken: loopersKey,
163
+ fetch: loopersFetch,
164
+ });
165
+ }
166
+ }
package/src/index.ts ADDED
@@ -0,0 +1 @@
1
+ export { LoopersOpenAI, LoopersAnthropic, LoopersClientOptions } from './client';
package/tsconfig.json ADDED
@@ -0,0 +1,15 @@
1
+ {
2
+ "compilerOptions": {
3
+ "target": "ES2020",
4
+ "module": "CommonJS",
5
+ "lib": ["ES2020", "DOM"],
6
+ "declaration": true,
7
+ "outDir": "./dist",
8
+ "rootDir": "./src",
9
+ "strict": true,
10
+ "esModuleInterop": true,
11
+ "skipLibCheck": true,
12
+ "forceConsistentCasingInFileNames": true
13
+ },
14
+ "include": ["src/**/*"]
15
+ }