@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 +79 -0
- package/dist/client.d.ts +20 -0
- package/dist/client.js +106 -0
- package/dist/index.d.ts +1 -0
- package/dist/index.js +6 -0
- package/package.json +38 -0
- package/src/client.ts +166 -0
- package/src/index.ts +1 -0
- package/tsconfig.json +15 -0
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
|
package/dist/client.d.ts
ADDED
|
@@ -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;
|
package/dist/index.d.ts
ADDED
|
@@ -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
|
+
}
|