@loopers/client 0.4.8 → 1.2.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/README.md +15 -0
- package/dist/client.d.ts +23 -0
- package/dist/client.js +40 -7
- package/dist/index.d.ts +1 -1
- package/dist/index.js +5 -1
- package/package.json +1 -1
- package/src/client.ts +53 -1
- package/src/index.ts +9 -1
- package/test/client.test.ts +111 -40
package/README.md
CHANGED
|
@@ -74,6 +74,21 @@ const response = await client.messages.create({
|
|
|
74
74
|
console.log(`Request Cost: $${(response as any).loopers_cost} USD`);
|
|
75
75
|
```
|
|
76
76
|
|
|
77
|
+
### Other Providers (Groq, Mistral, DeepSeek, Together)
|
|
78
|
+
|
|
79
|
+
Because these providers are OpenAI-compatible, they share the exact same interface as `LoopersOpenAI`. Simply substitute the class name:
|
|
80
|
+
|
|
81
|
+
```typescript
|
|
82
|
+
import { LoopersGroq } from '@loopers/client';
|
|
83
|
+
|
|
84
|
+
// Example using Groq
|
|
85
|
+
const client = new LoopersGroq({
|
|
86
|
+
loopersUrl: 'http://localhost:8080',
|
|
87
|
+
loopersKey: 'lp-xxx',
|
|
88
|
+
providerKey: 'gsk_xxx'
|
|
89
|
+
});
|
|
90
|
+
```
|
|
91
|
+
|
|
77
92
|
## License
|
|
78
93
|
|
|
79
94
|
MIT
|
package/dist/client.d.ts
CHANGED
|
@@ -7,12 +7,35 @@ export interface LoopersClientOptions {
|
|
|
7
7
|
sessionId?: string;
|
|
8
8
|
sessionBudget?: number;
|
|
9
9
|
maxSteps?: number;
|
|
10
|
+
sessionTtl?: number;
|
|
11
|
+
maxTools?: number;
|
|
12
|
+
maxServers?: number;
|
|
10
13
|
}
|
|
11
14
|
export declare class LoopersOpenAI extends OpenAI {
|
|
12
15
|
constructor(options: LoopersClientOptions & {
|
|
13
16
|
[key: string]: any;
|
|
14
17
|
});
|
|
15
18
|
}
|
|
19
|
+
export declare class LoopersGroq extends LoopersOpenAI {
|
|
20
|
+
constructor(options: LoopersClientOptions & {
|
|
21
|
+
[key: string]: any;
|
|
22
|
+
});
|
|
23
|
+
}
|
|
24
|
+
export declare class LoopersMistral extends LoopersOpenAI {
|
|
25
|
+
constructor(options: LoopersClientOptions & {
|
|
26
|
+
[key: string]: any;
|
|
27
|
+
});
|
|
28
|
+
}
|
|
29
|
+
export declare class LoopersDeepSeek extends LoopersOpenAI {
|
|
30
|
+
constructor(options: LoopersClientOptions & {
|
|
31
|
+
[key: string]: any;
|
|
32
|
+
});
|
|
33
|
+
}
|
|
34
|
+
export declare class LoopersTogether extends LoopersOpenAI {
|
|
35
|
+
constructor(options: LoopersClientOptions & {
|
|
36
|
+
[key: string]: any;
|
|
37
|
+
});
|
|
38
|
+
}
|
|
16
39
|
export declare class LoopersAnthropic extends Anthropic {
|
|
17
40
|
constructor(options: LoopersClientOptions & {
|
|
18
41
|
[key: string]: any;
|
package/dist/client.js
CHANGED
|
@@ -3,10 +3,10 @@ var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
|
3
3
|
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
4
4
|
};
|
|
5
5
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
6
|
-
exports.LoopersAnthropic = exports.LoopersOpenAI = void 0;
|
|
6
|
+
exports.LoopersAnthropic = exports.LoopersTogether = exports.LoopersDeepSeek = exports.LoopersMistral = exports.LoopersGroq = exports.LoopersOpenAI = void 0;
|
|
7
7
|
const openai_1 = __importDefault(require("openai"));
|
|
8
8
|
const sdk_1 = __importDefault(require("@anthropic-ai/sdk"));
|
|
9
|
-
function createLoopersFetch(loopersKey, providerKey, sessionId, sessionBudget, maxSteps, customFetch) {
|
|
9
|
+
function createLoopersFetch(loopersKey, providerKey, sessionId, sessionBudget, maxSteps, sessionTtl, maxTools, maxServers, customFetch) {
|
|
10
10
|
const originalFetch = customFetch || (typeof fetch !== 'undefined' ? fetch : undefined);
|
|
11
11
|
if (!originalFetch) {
|
|
12
12
|
throw new Error('A global fetch function is not available. Please pass a custom fetch implementation (e.g. node-fetch) or use Node.js 18+.');
|
|
@@ -28,6 +28,15 @@ function createLoopersFetch(loopersKey, providerKey, sessionId, sessionBudget, m
|
|
|
28
28
|
if (maxSteps !== undefined) {
|
|
29
29
|
headers.set('X-Loopers-Session-Max-Steps', String(maxSteps));
|
|
30
30
|
}
|
|
31
|
+
if (sessionTtl !== undefined) {
|
|
32
|
+
headers.set('X-Loopers-Session-TTL', String(sessionTtl));
|
|
33
|
+
}
|
|
34
|
+
if (maxTools !== undefined) {
|
|
35
|
+
headers.set('X-Loopers-Session-Max-Tools', String(maxTools));
|
|
36
|
+
}
|
|
37
|
+
if (maxServers !== undefined) {
|
|
38
|
+
headers.set('X-Loopers-Session-Max-Servers', String(maxServers));
|
|
39
|
+
}
|
|
31
40
|
const modifiedInit = {
|
|
32
41
|
...init,
|
|
33
42
|
headers,
|
|
@@ -78,23 +87,47 @@ function createLoopersFetch(loopersKey, providerKey, sessionId, sessionBudget, m
|
|
|
78
87
|
}
|
|
79
88
|
class LoopersOpenAI extends openai_1.default {
|
|
80
89
|
constructor(options) {
|
|
81
|
-
const { loopersUrl, loopersKey, providerKey, sessionId, sessionBudget, maxSteps, ...openaiOptions } = options;
|
|
90
|
+
const { loopersUrl, loopersKey, providerKey, sessionId, sessionBudget, maxSteps, sessionTtl, maxTools, maxServers, _providerPath, ...openaiOptions } = options;
|
|
82
91
|
const baseFetch = openaiOptions.fetch;
|
|
83
|
-
const loopersFetch = createLoopersFetch(loopersKey, providerKey, sessionId, sessionBudget, maxSteps, baseFetch);
|
|
92
|
+
const loopersFetch = createLoopersFetch(loopersKey, providerKey, sessionId, sessionBudget, maxSteps, sessionTtl, maxTools, maxServers, baseFetch);
|
|
84
93
|
super({
|
|
85
94
|
...openaiOptions,
|
|
86
|
-
baseURL: `${loopersUrl.replace(/\/$/, '')}
|
|
95
|
+
baseURL: `${loopersUrl.replace(/\/$/, '')}/${_providerPath || 'openai/v1'}`,
|
|
87
96
|
apiKey: loopersKey,
|
|
88
97
|
fetch: loopersFetch,
|
|
89
98
|
});
|
|
90
99
|
}
|
|
91
100
|
}
|
|
92
101
|
exports.LoopersOpenAI = LoopersOpenAI;
|
|
102
|
+
class LoopersGroq extends LoopersOpenAI {
|
|
103
|
+
constructor(options) {
|
|
104
|
+
super({ ...options, _providerPath: 'groq/v1' });
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
exports.LoopersGroq = LoopersGroq;
|
|
108
|
+
class LoopersMistral extends LoopersOpenAI {
|
|
109
|
+
constructor(options) {
|
|
110
|
+
super({ ...options, _providerPath: 'mistral/v1' });
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
exports.LoopersMistral = LoopersMistral;
|
|
114
|
+
class LoopersDeepSeek extends LoopersOpenAI {
|
|
115
|
+
constructor(options) {
|
|
116
|
+
super({ ...options, _providerPath: 'deepseek/v1' });
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
exports.LoopersDeepSeek = LoopersDeepSeek;
|
|
120
|
+
class LoopersTogether extends LoopersOpenAI {
|
|
121
|
+
constructor(options) {
|
|
122
|
+
super({ ...options, _providerPath: 'together/v1' });
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
exports.LoopersTogether = LoopersTogether;
|
|
93
126
|
class LoopersAnthropic extends sdk_1.default {
|
|
94
127
|
constructor(options) {
|
|
95
|
-
const { loopersUrl, loopersKey, providerKey, sessionId, sessionBudget, maxSteps, ...anthropicOptions } = options;
|
|
128
|
+
const { loopersUrl, loopersKey, providerKey, sessionId, sessionBudget, maxSteps, sessionTtl, maxTools, maxServers, ...anthropicOptions } = options;
|
|
96
129
|
const baseFetch = anthropicOptions.fetch;
|
|
97
|
-
const loopersFetch = createLoopersFetch(loopersKey, providerKey, sessionId, sessionBudget, maxSteps, baseFetch);
|
|
130
|
+
const loopersFetch = createLoopersFetch(loopersKey, providerKey, sessionId, sessionBudget, maxSteps, sessionTtl, maxTools, maxServers, baseFetch);
|
|
98
131
|
super({
|
|
99
132
|
...anthropicOptions,
|
|
100
133
|
baseURL: `${loopersUrl.replace(/\/$/, '')}/anthropic`,
|
package/dist/index.d.ts
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
export { LoopersOpenAI, LoopersAnthropic, LoopersClientOptions } from './client';
|
|
1
|
+
export { LoopersOpenAI, LoopersAnthropic, LoopersGroq, LoopersMistral, LoopersDeepSeek, LoopersTogether, LoopersClientOptions, } from './client';
|
package/dist/index.js
CHANGED
|
@@ -1,6 +1,10 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
-
exports.LoopersAnthropic = exports.LoopersOpenAI = void 0;
|
|
3
|
+
exports.LoopersTogether = exports.LoopersDeepSeek = exports.LoopersMistral = exports.LoopersGroq = exports.LoopersAnthropic = exports.LoopersOpenAI = void 0;
|
|
4
4
|
var client_1 = require("./client");
|
|
5
5
|
Object.defineProperty(exports, "LoopersOpenAI", { enumerable: true, get: function () { return client_1.LoopersOpenAI; } });
|
|
6
6
|
Object.defineProperty(exports, "LoopersAnthropic", { enumerable: true, get: function () { return client_1.LoopersAnthropic; } });
|
|
7
|
+
Object.defineProperty(exports, "LoopersGroq", { enumerable: true, get: function () { return client_1.LoopersGroq; } });
|
|
8
|
+
Object.defineProperty(exports, "LoopersMistral", { enumerable: true, get: function () { return client_1.LoopersMistral; } });
|
|
9
|
+
Object.defineProperty(exports, "LoopersDeepSeek", { enumerable: true, get: function () { return client_1.LoopersDeepSeek; } });
|
|
10
|
+
Object.defineProperty(exports, "LoopersTogether", { enumerable: true, get: function () { return client_1.LoopersTogether; } });
|
package/package.json
CHANGED
package/src/client.ts
CHANGED
|
@@ -8,6 +8,9 @@ export interface LoopersClientOptions {
|
|
|
8
8
|
sessionId?: string;
|
|
9
9
|
sessionBudget?: number;
|
|
10
10
|
maxSteps?: number;
|
|
11
|
+
sessionTtl?: number;
|
|
12
|
+
maxTools?: number;
|
|
13
|
+
maxServers?: number;
|
|
11
14
|
}
|
|
12
15
|
|
|
13
16
|
function createLoopersFetch(
|
|
@@ -16,6 +19,9 @@ function createLoopersFetch(
|
|
|
16
19
|
sessionId?: string,
|
|
17
20
|
sessionBudget?: number,
|
|
18
21
|
maxSteps?: number,
|
|
22
|
+
sessionTtl?: number,
|
|
23
|
+
maxTools?: number,
|
|
24
|
+
maxServers?: number,
|
|
19
25
|
customFetch?: typeof fetch
|
|
20
26
|
) {
|
|
21
27
|
const originalFetch = customFetch || (typeof fetch !== 'undefined' ? fetch : undefined);
|
|
@@ -41,6 +47,15 @@ function createLoopersFetch(
|
|
|
41
47
|
if (maxSteps !== undefined) {
|
|
42
48
|
headers.set('X-Loopers-Session-Max-Steps', String(maxSteps));
|
|
43
49
|
}
|
|
50
|
+
if (sessionTtl !== undefined) {
|
|
51
|
+
headers.set('X-Loopers-Session-TTL', String(sessionTtl));
|
|
52
|
+
}
|
|
53
|
+
if (maxTools !== undefined) {
|
|
54
|
+
headers.set('X-Loopers-Session-Max-Tools', String(maxTools));
|
|
55
|
+
}
|
|
56
|
+
if (maxServers !== undefined) {
|
|
57
|
+
headers.set('X-Loopers-Session-Max-Servers', String(maxServers));
|
|
58
|
+
}
|
|
44
59
|
|
|
45
60
|
const modifiedInit = {
|
|
46
61
|
...init,
|
|
@@ -108,6 +123,10 @@ export class LoopersOpenAI extends OpenAI {
|
|
|
108
123
|
sessionId,
|
|
109
124
|
sessionBudget,
|
|
110
125
|
maxSteps,
|
|
126
|
+
sessionTtl,
|
|
127
|
+
maxTools,
|
|
128
|
+
maxServers,
|
|
129
|
+
_providerPath,
|
|
111
130
|
...openaiOptions
|
|
112
131
|
} = options;
|
|
113
132
|
|
|
@@ -118,18 +137,45 @@ export class LoopersOpenAI extends OpenAI {
|
|
|
118
137
|
sessionId,
|
|
119
138
|
sessionBudget,
|
|
120
139
|
maxSteps,
|
|
140
|
+
sessionTtl,
|
|
141
|
+
maxTools,
|
|
142
|
+
maxServers,
|
|
121
143
|
baseFetch
|
|
122
144
|
);
|
|
123
145
|
|
|
124
146
|
super({
|
|
125
147
|
...openaiOptions,
|
|
126
|
-
baseURL: `${loopersUrl.replace(/\/$/, '')}
|
|
148
|
+
baseURL: `${loopersUrl.replace(/\/$/, '')}/${_providerPath || 'openai/v1'}`,
|
|
127
149
|
apiKey: loopersKey,
|
|
128
150
|
fetch: loopersFetch,
|
|
129
151
|
});
|
|
130
152
|
}
|
|
131
153
|
}
|
|
132
154
|
|
|
155
|
+
export class LoopersGroq extends LoopersOpenAI {
|
|
156
|
+
constructor(options: LoopersClientOptions & { [key: string]: any }) {
|
|
157
|
+
super({ ...options, _providerPath: 'groq/v1' });
|
|
158
|
+
}
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
export class LoopersMistral extends LoopersOpenAI {
|
|
162
|
+
constructor(options: LoopersClientOptions & { [key: string]: any }) {
|
|
163
|
+
super({ ...options, _providerPath: 'mistral/v1' });
|
|
164
|
+
}
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
export class LoopersDeepSeek extends LoopersOpenAI {
|
|
168
|
+
constructor(options: LoopersClientOptions & { [key: string]: any }) {
|
|
169
|
+
super({ ...options, _providerPath: 'deepseek/v1' });
|
|
170
|
+
}
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
export class LoopersTogether extends LoopersOpenAI {
|
|
174
|
+
constructor(options: LoopersClientOptions & { [key: string]: any }) {
|
|
175
|
+
super({ ...options, _providerPath: 'together/v1' });
|
|
176
|
+
}
|
|
177
|
+
}
|
|
178
|
+
|
|
133
179
|
export class LoopersAnthropic extends Anthropic {
|
|
134
180
|
constructor(
|
|
135
181
|
options: LoopersClientOptions & {
|
|
@@ -143,6 +189,9 @@ export class LoopersAnthropic extends Anthropic {
|
|
|
143
189
|
sessionId,
|
|
144
190
|
sessionBudget,
|
|
145
191
|
maxSteps,
|
|
192
|
+
sessionTtl,
|
|
193
|
+
maxTools,
|
|
194
|
+
maxServers,
|
|
146
195
|
...anthropicOptions
|
|
147
196
|
} = options;
|
|
148
197
|
|
|
@@ -153,6 +202,9 @@ export class LoopersAnthropic extends Anthropic {
|
|
|
153
202
|
sessionId,
|
|
154
203
|
sessionBudget,
|
|
155
204
|
maxSteps,
|
|
205
|
+
sessionTtl,
|
|
206
|
+
maxTools,
|
|
207
|
+
maxServers,
|
|
156
208
|
baseFetch
|
|
157
209
|
);
|
|
158
210
|
|
package/src/index.ts
CHANGED
package/test/client.test.ts
CHANGED
|
@@ -1,16 +1,40 @@
|
|
|
1
1
|
import { describe, it, expect, vi } from 'vitest';
|
|
2
|
-
import { LoopersOpenAI, LoopersAnthropic } from '../src/client';
|
|
2
|
+
import { LoopersOpenAI, LoopersAnthropic, LoopersGroq } from '../src/client';
|
|
3
|
+
|
|
4
|
+
// ---------------------------------------------------------------------------
|
|
5
|
+
// Helpers
|
|
6
|
+
// ---------------------------------------------------------------------------
|
|
7
|
+
|
|
8
|
+
function makeMockFetch(responseHeaders: Record<string, string> = {}) {
|
|
9
|
+
return vi.fn().mockResolvedValue({
|
|
10
|
+
json: async () => ({ id: 'chatcmpl-123', choices: [], object: 'chat.completion' }),
|
|
11
|
+
text: async () => JSON.stringify({ id: 'chatcmpl-123', choices: [], object: 'chat.completion' }),
|
|
12
|
+
headers: new Headers(responseHeaders),
|
|
13
|
+
ok: true,
|
|
14
|
+
status: 200,
|
|
15
|
+
});
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
// ---------------------------------------------------------------------------
|
|
19
|
+
// LoopersOpenAI
|
|
20
|
+
// ---------------------------------------------------------------------------
|
|
3
21
|
|
|
4
22
|
describe('LoopersOpenAI', () => {
|
|
5
|
-
it('should
|
|
6
|
-
const mockFetch =
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
status: 200,
|
|
23
|
+
it('should route to the correct URL', async () => {
|
|
24
|
+
const mockFetch = makeMockFetch();
|
|
25
|
+
const client = new LoopersOpenAI({
|
|
26
|
+
loopersUrl: 'http://localhost:8080',
|
|
27
|
+
loopersKey: 'lp-123',
|
|
28
|
+
fetch: mockFetch as any,
|
|
12
29
|
});
|
|
30
|
+
await client.chat.completions.create({ model: 'gpt-4', messages: [{ role: 'user', content: 'hi' }] });
|
|
13
31
|
|
|
32
|
+
const [url] = mockFetch.mock.calls[0];
|
|
33
|
+
expect(url.toString()).toBe('http://localhost:8080/openai/v1/chat/completions');
|
|
34
|
+
});
|
|
35
|
+
|
|
36
|
+
it('should inject all governance headers', async () => {
|
|
37
|
+
const mockFetch = makeMockFetch();
|
|
14
38
|
const client = new LoopersOpenAI({
|
|
15
39
|
loopersUrl: 'http://localhost:8080',
|
|
16
40
|
loopersKey: 'lp-123',
|
|
@@ -18,60 +42,70 @@ describe('LoopersOpenAI', () => {
|
|
|
18
42
|
sessionId: 'sess-1',
|
|
19
43
|
sessionBudget: 5.0,
|
|
20
44
|
maxSteps: 10,
|
|
45
|
+
sessionTtl: 3600,
|
|
46
|
+
maxTools: 5,
|
|
47
|
+
maxServers: 2,
|
|
21
48
|
fetch: mockFetch as any,
|
|
22
49
|
});
|
|
50
|
+
await client.chat.completions.create({ model: 'gpt-4', messages: [{ role: 'user', content: 'hi' }] });
|
|
23
51
|
|
|
24
|
-
|
|
25
|
-
model: 'gpt-4',
|
|
26
|
-
messages: [{ role: 'user', content: 'hi' }],
|
|
27
|
-
});
|
|
28
|
-
|
|
29
|
-
expect(mockFetch).toHaveBeenCalledTimes(1);
|
|
30
|
-
const [url, init] = mockFetch.mock.calls[0];
|
|
31
|
-
expect(url.toString()).toBe('http://localhost:8080/openai/v1/chat/completions');
|
|
32
|
-
|
|
52
|
+
const [, init] = mockFetch.mock.calls[0];
|
|
33
53
|
const headers = new Headers(init.headers);
|
|
34
54
|
expect(headers.get('Authorization')).toBe('Bearer lp-123');
|
|
35
55
|
expect(headers.get('X-Loopers-Provider-Key')).toBe('sk-openai');
|
|
36
56
|
expect(headers.get('X-Loopers-Session-ID')).toBe('sess-1');
|
|
37
57
|
expect(headers.get('X-Loopers-Session-Budget')).toBe('5');
|
|
38
58
|
expect(headers.get('X-Loopers-Session-Max-Steps')).toBe('10');
|
|
59
|
+
expect(headers.get('X-Loopers-Session-TTL')).toBe('3600');
|
|
60
|
+
expect(headers.get('X-Loopers-Session-Max-Tools')).toBe('5');
|
|
61
|
+
expect(headers.get('X-Loopers-Session-Max-Servers')).toBe('2');
|
|
39
62
|
});
|
|
40
63
|
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
// are never attached to the returned object. Skipping this test until the SDK is redesigned.
|
|
44
|
-
it.skip('should parse loopers metrics from headers', async () => {
|
|
45
|
-
|
|
46
|
-
const mockFetch = vi.fn().mockResolvedValue({
|
|
47
|
-
json: async () => ({ id: 'chatcmpl-123' }),
|
|
48
|
-
text: async () => JSON.stringify({ id: 'chatcmpl-123' }),
|
|
49
|
-
headers: mockHeaders,
|
|
50
|
-
ok: true,
|
|
51
|
-
status: 200,
|
|
52
|
-
});
|
|
53
|
-
|
|
64
|
+
it('should NOT send optional headers when they are not set', async () => {
|
|
65
|
+
const mockFetch = makeMockFetch();
|
|
54
66
|
const client = new LoopersOpenAI({
|
|
55
67
|
loopersUrl: 'http://localhost:8080',
|
|
56
68
|
loopersKey: 'lp-123',
|
|
57
69
|
fetch: mockFetch as any,
|
|
58
70
|
});
|
|
71
|
+
await client.chat.completions.create({ model: 'gpt-4', messages: [{ role: 'user', content: 'hi' }] });
|
|
59
72
|
|
|
60
|
-
const
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
73
|
+
const [, init] = mockFetch.mock.calls[0];
|
|
74
|
+
const headers = new Headers(init.headers);
|
|
75
|
+
expect(headers.get('X-Loopers-Session-Budget')).toBeNull();
|
|
76
|
+
expect(headers.get('X-Loopers-Session-Max-Steps')).toBeNull();
|
|
77
|
+
expect(headers.get('X-Loopers-Session-TTL')).toBeNull();
|
|
78
|
+
expect(headers.get('X-Loopers-Session-Max-Tools')).toBeNull();
|
|
79
|
+
expect(headers.get('X-Loopers-Session-Max-Servers')).toBeNull();
|
|
80
|
+
});
|
|
64
81
|
|
|
82
|
+
// NOTE: The TS SDK intercepts res.json() but the underlying OpenAI SDK
|
|
83
|
+
// internally uses res.text() + JSON.parse(), so metrics are not attached
|
|
84
|
+
// to the returned typed object. This is a known limitation documented as
|
|
85
|
+
// a skipped test — the raw fetch response does expose the headers correctly.
|
|
86
|
+
it.skip('should parse loopers metrics from response headers', async () => {
|
|
87
|
+
const mockFetch = vi.fn().mockResolvedValue({
|
|
88
|
+
json: async () => ({ id: 'chatcmpl-123', choices: [], object: 'chat.completion' }),
|
|
89
|
+
text: async () => JSON.stringify({ id: 'chatcmpl-123', choices: [], object: 'chat.completion' }),
|
|
90
|
+
headers: new Headers({ 'X-Loopers-Request-Cost': '0.01', 'X-Loopers-Session-Spend': '0.05' }),
|
|
91
|
+
ok: true,
|
|
92
|
+
status: 200,
|
|
93
|
+
});
|
|
94
|
+
const client = new LoopersOpenAI({ loopersUrl: 'http://localhost:8080', loopersKey: 'lp-123', fetch: mockFetch as any });
|
|
95
|
+
const res = await client.chat.completions.create({ model: 'gpt-4', messages: [{ role: 'user', content: 'hi' }] }) as any;
|
|
65
96
|
expect(res.loopers_cost).toBe(0.01);
|
|
66
|
-
expect(res.loopers_session_spend).toBe(0.05);
|
|
67
97
|
});
|
|
68
98
|
});
|
|
69
99
|
|
|
100
|
+
// ---------------------------------------------------------------------------
|
|
101
|
+
// LoopersAnthropic
|
|
102
|
+
// ---------------------------------------------------------------------------
|
|
103
|
+
|
|
70
104
|
describe('LoopersAnthropic', () => {
|
|
71
|
-
it('should
|
|
105
|
+
it('should route to the correct URL and inject governance headers', async () => {
|
|
72
106
|
const mockFetch = vi.fn().mockResolvedValue({
|
|
73
|
-
json: async () => ({ id: 'msg_123', type: 'message' }),
|
|
74
|
-
text: async () => JSON.stringify({ id: 'msg_123', type: 'message' }),
|
|
107
|
+
json: async () => ({ id: 'msg_123', type: 'message', role: 'assistant', content: [], model: 'claude-3', stop_reason: 'end_turn', usage: { input_tokens: 1, output_tokens: 1 } }),
|
|
108
|
+
text: async () => JSON.stringify({ id: 'msg_123', type: 'message', role: 'assistant', content: [], model: 'claude-3', stop_reason: 'end_turn', usage: { input_tokens: 1, output_tokens: 1 } }),
|
|
75
109
|
headers: new Headers(),
|
|
76
110
|
ok: true,
|
|
77
111
|
status: 200,
|
|
@@ -81,6 +115,12 @@ describe('LoopersAnthropic', () => {
|
|
|
81
115
|
loopersUrl: 'http://localhost:8080',
|
|
82
116
|
loopersKey: 'lp-123',
|
|
83
117
|
providerKey: 'sk-ant',
|
|
118
|
+
sessionId: 'sess-ant-1',
|
|
119
|
+
sessionBudget: 10.0,
|
|
120
|
+
maxSteps: 5,
|
|
121
|
+
sessionTtl: 1800,
|
|
122
|
+
maxTools: 3,
|
|
123
|
+
maxServers: 1,
|
|
84
124
|
fetch: mockFetch as any,
|
|
85
125
|
});
|
|
86
126
|
|
|
@@ -90,12 +130,43 @@ describe('LoopersAnthropic', () => {
|
|
|
90
130
|
max_tokens: 10,
|
|
91
131
|
});
|
|
92
132
|
|
|
93
|
-
expect(mockFetch).toHaveBeenCalledTimes(1);
|
|
94
133
|
const [url, init] = mockFetch.mock.calls[0];
|
|
95
134
|
expect(url.toString()).toBe('http://localhost:8080/anthropic/v1/messages');
|
|
96
|
-
|
|
97
135
|
const headers = new Headers(init.headers);
|
|
98
136
|
expect(headers.get('Authorization')).toBe('Bearer lp-123');
|
|
99
137
|
expect(headers.get('X-Loopers-Provider-Key')).toBe('sk-ant');
|
|
138
|
+
expect(headers.get('X-Loopers-Session-ID')).toBe('sess-ant-1');
|
|
139
|
+
expect(headers.get('X-Loopers-Session-Budget')).toBe('10');
|
|
140
|
+
expect(headers.get('X-Loopers-Session-Max-Steps')).toBe('5');
|
|
141
|
+
expect(headers.get('X-Loopers-Session-TTL')).toBe('1800');
|
|
142
|
+
expect(headers.get('X-Loopers-Session-Max-Tools')).toBe('3');
|
|
143
|
+
expect(headers.get('X-Loopers-Session-Max-Servers')).toBe('1');
|
|
144
|
+
});
|
|
145
|
+
});
|
|
146
|
+
|
|
147
|
+
// ---------------------------------------------------------------------------
|
|
148
|
+
// Provider subclasses
|
|
149
|
+
// ---------------------------------------------------------------------------
|
|
150
|
+
|
|
151
|
+
describe('LoopersGroq', () => {
|
|
152
|
+
it('should route to groq/v1 path with all new headers', async () => {
|
|
153
|
+
const mockFetch = makeMockFetch();
|
|
154
|
+
const client = new LoopersGroq({
|
|
155
|
+
loopersUrl: 'http://localhost:8080',
|
|
156
|
+
loopersKey: 'lp-123',
|
|
157
|
+
providerKey: 'gsk_123',
|
|
158
|
+
sessionTtl: 600,
|
|
159
|
+
maxTools: 10,
|
|
160
|
+
maxServers: 3,
|
|
161
|
+
fetch: mockFetch as any,
|
|
162
|
+
});
|
|
163
|
+
await client.chat.completions.create({ model: 'llama-3', messages: [{ role: 'user', content: 'hi' }] });
|
|
164
|
+
|
|
165
|
+
const [url, init] = mockFetch.mock.calls[0];
|
|
166
|
+
expect(url.toString()).toBe('http://localhost:8080/groq/v1/chat/completions');
|
|
167
|
+
const headers = new Headers(init.headers);
|
|
168
|
+
expect(headers.get('X-Loopers-Session-TTL')).toBe('600');
|
|
169
|
+
expect(headers.get('X-Loopers-Session-Max-Tools')).toBe('10');
|
|
170
|
+
expect(headers.get('X-Loopers-Session-Max-Servers')).toBe('3');
|
|
100
171
|
});
|
|
101
172
|
});
|