@loopers/client 1.0.0 → 1.3.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/CHANGELOG.md +22 -0
- package/dist/client.d.ts +3 -0
- package/dist/client.js +14 -5
- package/package.json +1 -1
- package/src/client.ts +27 -0
- package/test/client.test.ts +97 -56
package/CHANGELOG.md
ADDED
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
# Changelog
|
|
2
|
+
|
|
3
|
+
## [1.3.0](https://github.com/CURSED-ME/loopers-oss/compare/sdk-ts-v1.2.0...sdk-ts-v1.3.0) (2026-06-28)
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
### Features
|
|
7
|
+
|
|
8
|
+
* add 14 providers, bump SDKs to 1.0.0, prepare for launch ([abd1d6d](https://github.com/CURSED-ME/loopers-oss/commit/abd1d6d06623bd4ba475247cc8f4d83d744e67f4))
|
|
9
|
+
* **sdk:** support Phase 1 session headers ([ec81008](https://github.com/CURSED-ME/loopers-oss/commit/ec81008939fb5c3d9e78fe210d2e6c766bfc99b6))
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
### Bug Fixes
|
|
13
|
+
|
|
14
|
+
* bump sdk versions and disable homebrew publishing for CI ([9e8ceae](https://github.com/CURSED-ME/loopers-oss/commit/9e8ceaef264fcde3dad46af132481f9db91cebb0))
|
|
15
|
+
* **ci:** disable homebrew tap until repo is created, bump versions to 0.4.5 ([8523ff4](https://github.com/CURSED-ME/loopers-oss/commit/8523ff498a82e8310a8cdc6f856e39b399ef7d5a))
|
|
16
|
+
* **ci:** fix goreleaser sbom config and bump versions to 0.4.3 ([56563ec](https://github.com/CURSED-ME/loopers-oss/commit/56563ece8d04927227308905b7105e396326880f))
|
|
17
|
+
* **ci:** fix invalid action commit hashes for goreleaser, bump version to 0.4.2 ([d6a40ef](https://github.com/CURSED-ME/loopers-oss/commit/d6a40efa1f51529046d3513368b9bea6e7d4ae54))
|
|
18
|
+
* **ci:** point syft to latest v0 to support --enrich flag and bump to v0.4.7 ([29a7f7f](https://github.com/CURSED-ME/loopers-oss/commit/29a7f7f7c2ae1ba293004db61a9743a2218b55d5))
|
|
19
|
+
* **ci:** use latest syft for goreleaser sboms, bump versions to 0.4.4 ([af0790a](https://github.com/CURSED-ME/loopers-oss/commit/af0790a6245cd9e9bf309b5747dfb89e6fe8c3f3))
|
|
20
|
+
* **sdk:** add Phase 1 headers to all adapters, bump to v1.2.0, expand test coverage ([9a40389](https://github.com/CURSED-ME/loopers-oss/commit/9a40389146df62674d53705f500851601181d1cb))
|
|
21
|
+
* **sdk:** fix anthropic sdk version in TS package, bump versions to 0.4.1 ([df374e6](https://github.com/CURSED-ME/loopers-oss/commit/df374e6d4e95aa09d77f24f94dd10dd41444c3b2))
|
|
22
|
+
* **security:** address OpenSSF scorecard issues and bump to v0.4.6 ([4d31477](https://github.com/CURSED-ME/loopers-oss/commit/4d3147704db87b8c11e988ddd17b06f18a658c9d))
|
package/dist/client.d.ts
CHANGED
|
@@ -7,6 +7,9 @@ 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 & {
|
package/dist/client.js
CHANGED
|
@@ -6,7 +6,7 @@ Object.defineProperty(exports, "__esModule", { value: true });
|
|
|
6
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,9 +87,9 @@ 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, _providerPath, ...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
95
|
baseURL: `${loopersUrl.replace(/\/$/, '')}/${_providerPath || 'openai/v1'}`,
|
|
@@ -116,9 +125,9 @@ class LoopersTogether extends LoopersOpenAI {
|
|
|
116
125
|
exports.LoopersTogether = LoopersTogether;
|
|
117
126
|
class LoopersAnthropic extends sdk_1.default {
|
|
118
127
|
constructor(options) {
|
|
119
|
-
const { loopersUrl, loopersKey, providerKey, sessionId, sessionBudget, maxSteps, ...anthropicOptions } = options;
|
|
128
|
+
const { loopersUrl, loopersKey, providerKey, sessionId, sessionBudget, maxSteps, sessionTtl, maxTools, maxServers, ...anthropicOptions } = options;
|
|
120
129
|
const baseFetch = anthropicOptions.fetch;
|
|
121
|
-
const loopersFetch = createLoopersFetch(loopersKey, providerKey, sessionId, sessionBudget, maxSteps, baseFetch);
|
|
130
|
+
const loopersFetch = createLoopersFetch(loopersKey, providerKey, sessionId, sessionBudget, maxSteps, sessionTtl, maxTools, maxServers, baseFetch);
|
|
122
131
|
super({
|
|
123
132
|
...anthropicOptions,
|
|
124
133
|
baseURL: `${loopersUrl.replace(/\/$/, '')}/anthropic`,
|
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,9 @@ export class LoopersOpenAI extends OpenAI {
|
|
|
108
123
|
sessionId,
|
|
109
124
|
sessionBudget,
|
|
110
125
|
maxSteps,
|
|
126
|
+
sessionTtl,
|
|
127
|
+
maxTools,
|
|
128
|
+
maxServers,
|
|
111
129
|
_providerPath,
|
|
112
130
|
...openaiOptions
|
|
113
131
|
} = options;
|
|
@@ -119,6 +137,9 @@ export class LoopersOpenAI extends OpenAI {
|
|
|
119
137
|
sessionId,
|
|
120
138
|
sessionBudget,
|
|
121
139
|
maxSteps,
|
|
140
|
+
sessionTtl,
|
|
141
|
+
maxTools,
|
|
142
|
+
maxServers,
|
|
122
143
|
baseFetch
|
|
123
144
|
);
|
|
124
145
|
|
|
@@ -168,6 +189,9 @@ export class LoopersAnthropic extends Anthropic {
|
|
|
168
189
|
sessionId,
|
|
169
190
|
sessionBudget,
|
|
170
191
|
maxSteps,
|
|
192
|
+
sessionTtl,
|
|
193
|
+
maxTools,
|
|
194
|
+
maxServers,
|
|
171
195
|
...anthropicOptions
|
|
172
196
|
} = options;
|
|
173
197
|
|
|
@@ -178,6 +202,9 @@ export class LoopersAnthropic extends Anthropic {
|
|
|
178
202
|
sessionId,
|
|
179
203
|
sessionBudget,
|
|
180
204
|
maxSteps,
|
|
205
|
+
sessionTtl,
|
|
206
|
+
maxTools,
|
|
207
|
+
maxServers,
|
|
181
208
|
baseFetch
|
|
182
209
|
);
|
|
183
210
|
|
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,42 +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');
|
|
100
144
|
});
|
|
101
145
|
});
|
|
102
146
|
|
|
103
|
-
|
|
147
|
+
// ---------------------------------------------------------------------------
|
|
148
|
+
// Provider subclasses
|
|
149
|
+
// ---------------------------------------------------------------------------
|
|
104
150
|
|
|
105
151
|
describe('LoopersGroq', () => {
|
|
106
|
-
it('should
|
|
107
|
-
const mockFetch =
|
|
108
|
-
json: async () => ({ id: 'chatcmpl-123' }),
|
|
109
|
-
text: async () => JSON.stringify({ id: 'chatcmpl-123' }),
|
|
110
|
-
headers: new Headers(),
|
|
111
|
-
ok: true,
|
|
112
|
-
status: 200,
|
|
113
|
-
});
|
|
114
|
-
|
|
152
|
+
it('should route to groq/v1 path with all new headers', async () => {
|
|
153
|
+
const mockFetch = makeMockFetch();
|
|
115
154
|
const client = new LoopersGroq({
|
|
116
155
|
loopersUrl: 'http://localhost:8080',
|
|
117
156
|
loopersKey: 'lp-123',
|
|
118
157
|
providerKey: 'gsk_123',
|
|
158
|
+
sessionTtl: 600,
|
|
159
|
+
maxTools: 10,
|
|
160
|
+
maxServers: 3,
|
|
119
161
|
fetch: mockFetch as any,
|
|
120
162
|
});
|
|
163
|
+
await client.chat.completions.create({ model: 'llama-3', messages: [{ role: 'user', content: 'hi' }] });
|
|
121
164
|
|
|
122
|
-
await client.chat.completions.create({
|
|
123
|
-
model: 'llama-3',
|
|
124
|
-
messages: [{ role: 'user', content: 'hi' }],
|
|
125
|
-
});
|
|
126
|
-
|
|
127
|
-
expect(mockFetch).toHaveBeenCalledTimes(1);
|
|
128
165
|
const [url, init] = mockFetch.mock.calls[0];
|
|
129
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');
|
|
130
171
|
});
|
|
131
172
|
});
|