@llumiverse/core 1.3.0 → 1.4.0-dev.20260629.090753Z
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/lib/CompletionStream.d.ts +48 -0
- package/lib/CompletionStream.d.ts.map +1 -0
- package/lib/{esm/CompletionStream.js → CompletionStream.js} +142 -67
- package/lib/CompletionStream.js.map +1 -0
- package/lib/{types/Driver.d.ts → Driver.d.ts} +38 -5
- package/lib/Driver.d.ts.map +1 -0
- package/lib/{esm/Driver.js → Driver.js} +151 -106
- package/lib/Driver.js.map +1 -0
- package/lib/{types/async.d.ts → async.d.ts} +3 -3
- package/lib/async.d.ts.map +1 -0
- package/lib/{esm/async.js → async.js} +4 -4
- package/lib/async.js.map +1 -0
- package/lib/{types/conversation-utils.d.ts → conversation-utils.d.ts} +9 -0
- package/lib/conversation-utils.d.ts.map +1 -0
- package/lib/{esm/conversation-utils.js → conversation-utils.js} +78 -15
- package/lib/conversation-utils.js.map +1 -0
- package/lib/embeddings.d.ts +58 -0
- package/lib/embeddings.d.ts.map +1 -0
- package/lib/embeddings.js +131 -0
- package/lib/embeddings.js.map +1 -0
- package/lib/formatters/commons.d.ts +3 -0
- package/lib/formatters/commons.d.ts.map +1 -0
- package/lib/formatters/commons.js +4 -0
- package/lib/formatters/commons.js.map +1 -0
- package/lib/{types/formatters → formatters}/generic.d.ts +2 -2
- package/lib/formatters/generic.d.ts.map +1 -0
- package/lib/{esm/formatters → formatters}/generic.js +8 -8
- package/lib/formatters/generic.js.map +1 -0
- package/lib/formatters/index.d.ts +4 -0
- package/lib/formatters/index.d.ts.map +1 -0
- package/lib/formatters/index.js +4 -0
- package/lib/formatters/index.js.map +1 -0
- package/lib/{types/formatters → formatters}/nova.d.ts +4 -4
- package/lib/formatters/nova.d.ts.map +1 -0
- package/lib/{esm/formatters → formatters}/nova.js +19 -17
- package/lib/formatters/nova.js.map +1 -0
- package/lib/http-agent.d.ts +53 -0
- package/lib/http-agent.d.ts.map +1 -0
- package/lib/http-agent.js +141 -0
- package/lib/http-agent.js.map +1 -0
- package/lib/index.d.ts +8 -0
- package/lib/index.d.ts.map +1 -0
- package/lib/index.js +7 -0
- package/lib/index.js.map +1 -0
- package/lib/{types/json.d.ts → json.d.ts} +1 -1
- package/lib/json.d.ts.map +1 -0
- package/lib/{esm/json.js → json.js} +4 -3
- package/lib/json.js.map +1 -0
- package/lib/logger.d.ts +3 -0
- package/lib/logger.d.ts.map +1 -0
- package/lib/logger.js +42 -0
- package/lib/logger.js.map +1 -0
- package/lib/resolver.d.ts +2 -0
- package/lib/resolver.d.ts.map +1 -0
- package/lib/{esm/resolver.js → resolver.js} +7 -4
- package/lib/resolver.js.map +1 -0
- package/lib/stream.d.ts.map +1 -0
- package/lib/stream.js.map +1 -0
- package/lib/{types/validation.d.ts → validation.d.ts} +1 -1
- package/lib/validation.d.ts.map +1 -0
- package/lib/validation.js +100 -0
- package/lib/validation.js.map +1 -0
- package/package.json +28 -38
- package/src/CompletionStream.ts +208 -115
- package/src/Driver.error.test.ts +101 -72
- package/src/Driver.ts +208 -152
- package/src/async.ts +30 -30
- package/src/conversation-utils.ts +92 -16
- package/src/embeddings.ts +157 -0
- package/src/formatters/commons.ts +2 -2
- package/src/formatters/generic.ts +20 -17
- package/src/formatters/index.ts +3 -3
- package/src/formatters/nova.ts +53 -51
- package/src/http-agent.test.ts +281 -0
- package/src/http-agent.ts +177 -0
- package/src/index.ts +7 -5
- package/src/json.ts +6 -5
- package/src/logger.ts +42 -0
- package/src/resolver.ts +12 -9
- package/src/stream.ts +4 -5
- package/src/validation.ts +42 -29
- package/lib/cjs/CompletionStream.js +0 -293
- package/lib/cjs/CompletionStream.js.map +0 -1
- package/lib/cjs/Driver.js +0 -300
- package/lib/cjs/Driver.js.map +0 -1
- package/lib/cjs/async.js +0 -131
- package/lib/cjs/async.js.map +0 -1
- package/lib/cjs/conversation-utils.js +0 -579
- package/lib/cjs/conversation-utils.js.map +0 -1
- package/lib/cjs/formatters/commons.js +0 -7
- package/lib/cjs/formatters/commons.js.map +0 -1
- package/lib/cjs/formatters/generic.js +0 -59
- package/lib/cjs/formatters/generic.js.map +0 -1
- package/lib/cjs/formatters/index.js +0 -20
- package/lib/cjs/formatters/index.js.map +0 -1
- package/lib/cjs/formatters/nova.js +0 -99
- package/lib/cjs/formatters/nova.js.map +0 -1
- package/lib/cjs/index.js +0 -22
- package/lib/cjs/index.js.map +0 -1
- package/lib/cjs/json.js +0 -29
- package/lib/cjs/json.js.map +0 -1
- package/lib/cjs/package.json +0 -3
- package/lib/cjs/resolver.js +0 -45
- package/lib/cjs/resolver.js.map +0 -1
- package/lib/cjs/stream.js +0 -30
- package/lib/cjs/stream.js.map +0 -1
- package/lib/cjs/validation.js +0 -96
- package/lib/cjs/validation.js.map +0 -1
- package/lib/esm/CompletionStream.js.map +0 -1
- package/lib/esm/Driver.js.map +0 -1
- package/lib/esm/async.js.map +0 -1
- package/lib/esm/conversation-utils.js.map +0 -1
- package/lib/esm/formatters/commons.js +0 -4
- package/lib/esm/formatters/commons.js.map +0 -1
- package/lib/esm/formatters/generic.js.map +0 -1
- package/lib/esm/formatters/index.js +0 -4
- package/lib/esm/formatters/index.js.map +0 -1
- package/lib/esm/formatters/nova.js.map +0 -1
- package/lib/esm/index.js +0 -6
- package/lib/esm/index.js.map +0 -1
- package/lib/esm/json.js.map +0 -1
- package/lib/esm/resolver.js.map +0 -1
- package/lib/esm/stream.js.map +0 -1
- package/lib/esm/validation.js +0 -88
- package/lib/esm/validation.js.map +0 -1
- package/lib/types/CompletionStream.d.ts +0 -20
- package/lib/types/CompletionStream.d.ts.map +0 -1
- package/lib/types/Driver.d.ts.map +0 -1
- package/lib/types/async.d.ts.map +0 -1
- package/lib/types/conversation-utils.d.ts.map +0 -1
- package/lib/types/formatters/commons.d.ts +0 -3
- package/lib/types/formatters/commons.d.ts.map +0 -1
- package/lib/types/formatters/generic.d.ts.map +0 -1
- package/lib/types/formatters/index.d.ts +0 -4
- package/lib/types/formatters/index.d.ts.map +0 -1
- package/lib/types/formatters/nova.d.ts.map +0 -1
- package/lib/types/index.d.ts +0 -6
- package/lib/types/index.d.ts.map +0 -1
- package/lib/types/json.d.ts.map +0 -1
- package/lib/types/resolver.d.ts +0 -2
- package/lib/types/resolver.d.ts.map +0 -1
- package/lib/types/stream.d.ts.map +0 -1
- package/lib/types/validation.d.ts.map +0 -1
- /package/lib/{types/stream.d.ts → stream.d.ts} +0 -0
- /package/lib/{esm/stream.js → stream.js} +0 -0
|
@@ -0,0 +1,281 @@
|
|
|
1
|
+
import { createServer, type IncomingMessage, type ServerResponse } from 'node:http';
|
|
2
|
+
import type { AddressInfo } from 'node:net';
|
|
3
|
+
import {
|
|
4
|
+
type AIModel,
|
|
5
|
+
type Completion,
|
|
6
|
+
type CompletionChunkObject,
|
|
7
|
+
type DriverOptions,
|
|
8
|
+
type EmbeddingsOptions,
|
|
9
|
+
type EmbeddingsResult,
|
|
10
|
+
type ExecutionOptions,
|
|
11
|
+
type ModelSearchPayload,
|
|
12
|
+
PromptRole,
|
|
13
|
+
} from '@llumiverse/common';
|
|
14
|
+
import type { Agent } from 'undici';
|
|
15
|
+
import { afterEach, describe, expect, it, vi } from 'vitest';
|
|
16
|
+
import { AbstractDriver } from './Driver.js';
|
|
17
|
+
import {
|
|
18
|
+
createAgentBackedFetch,
|
|
19
|
+
createDriverHttpAgent,
|
|
20
|
+
DEFAULT_DRIVER_HTTP_TIMEOUTS,
|
|
21
|
+
mergeDriverHttpTimeoutOptions,
|
|
22
|
+
resolveDriverHttpTimeouts,
|
|
23
|
+
} from './http-agent.js';
|
|
24
|
+
|
|
25
|
+
class TestDriver extends AbstractDriver<DriverOptions, string> {
|
|
26
|
+
provider = 'test';
|
|
27
|
+
|
|
28
|
+
getAgent(): Agent {
|
|
29
|
+
return this.getHttpAgent();
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
getFetch(): typeof fetch {
|
|
33
|
+
return this.getDriverFetch();
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
async requestTextCompletion(prompt: string, _options: ExecutionOptions): Promise<Completion> {
|
|
37
|
+
const response = await this.getDriverFetch()(prompt);
|
|
38
|
+
return {
|
|
39
|
+
result: [{ type: 'text', value: await response.text() }],
|
|
40
|
+
};
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
async requestTextCompletionStream(
|
|
44
|
+
_prompt: string,
|
|
45
|
+
_options: ExecutionOptions,
|
|
46
|
+
): Promise<AsyncIterable<CompletionChunkObject>> {
|
|
47
|
+
throw new Error('Not implemented');
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
async listModels(_params?: ModelSearchPayload): Promise<AIModel[]> {
|
|
51
|
+
return [];
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
async validateConnection(): Promise<boolean> {
|
|
55
|
+
return true;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
async generateEmbeddings(_options: EmbeddingsOptions): Promise<EmbeddingsResult> {
|
|
59
|
+
throw new Error('Not implemented');
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
type TestServer = {
|
|
64
|
+
url: string;
|
|
65
|
+
close: () => Promise<void>;
|
|
66
|
+
};
|
|
67
|
+
|
|
68
|
+
async function startServer(handler: (req: IncomingMessage, res: ServerResponse) => void): Promise<TestServer> {
|
|
69
|
+
const server = createServer(handler);
|
|
70
|
+
|
|
71
|
+
await new Promise<void>((resolve, reject) => {
|
|
72
|
+
server.once('error', reject);
|
|
73
|
+
server.listen(0, '127.0.0.1', resolve);
|
|
74
|
+
});
|
|
75
|
+
|
|
76
|
+
const address = server.address();
|
|
77
|
+
if (!address || typeof address === 'string') {
|
|
78
|
+
throw new Error('Unable to resolve test server port');
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
return {
|
|
82
|
+
url: `http://127.0.0.1:${(address as AddressInfo).port}`,
|
|
83
|
+
close: () =>
|
|
84
|
+
new Promise<void>((resolve, reject) => {
|
|
85
|
+
server.close((error?: Error) => (error ? reject(error) : resolve()));
|
|
86
|
+
}),
|
|
87
|
+
};
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
describe('driver HTTP agent helpers', () => {
|
|
91
|
+
afterEach(() => {
|
|
92
|
+
vi.restoreAllMocks();
|
|
93
|
+
});
|
|
94
|
+
|
|
95
|
+
it('fills missing timeout options from defaults', () => {
|
|
96
|
+
expect(resolveDriverHttpTimeouts()).toEqual(DEFAULT_DRIVER_HTTP_TIMEOUTS);
|
|
97
|
+
expect(
|
|
98
|
+
resolveDriverHttpTimeouts({
|
|
99
|
+
headersTimeout: 123,
|
|
100
|
+
keepAliveTimeout: 456,
|
|
101
|
+
}),
|
|
102
|
+
).toEqual({
|
|
103
|
+
headersTimeout: 123,
|
|
104
|
+
bodyTimeout: DEFAULT_DRIVER_HTTP_TIMEOUTS.bodyTimeout,
|
|
105
|
+
connectTimeout: DEFAULT_DRIVER_HTTP_TIMEOUTS.connectTimeout,
|
|
106
|
+
keepAliveTimeout: 456,
|
|
107
|
+
});
|
|
108
|
+
});
|
|
109
|
+
|
|
110
|
+
it('merges per-call timeout overrides without replacing defined defaults with undefined', () => {
|
|
111
|
+
expect(
|
|
112
|
+
mergeDriverHttpTimeoutOptions(
|
|
113
|
+
{
|
|
114
|
+
headersTimeout: 1_000,
|
|
115
|
+
bodyTimeout: 2_000,
|
|
116
|
+
connectTimeout: 300,
|
|
117
|
+
},
|
|
118
|
+
{
|
|
119
|
+
headersTimeout: undefined,
|
|
120
|
+
bodyTimeout: 500,
|
|
121
|
+
},
|
|
122
|
+
),
|
|
123
|
+
).toEqual({
|
|
124
|
+
headersTimeout: 1_000,
|
|
125
|
+
bodyTimeout: 500,
|
|
126
|
+
connectTimeout: 300,
|
|
127
|
+
});
|
|
128
|
+
});
|
|
129
|
+
|
|
130
|
+
it('accepts global Request instances without parsing them as strings', async () => {
|
|
131
|
+
const server = await startServer((_req, res) => {
|
|
132
|
+
res.writeHead(200, { 'content-type': 'text/plain' });
|
|
133
|
+
res.end('ok');
|
|
134
|
+
});
|
|
135
|
+
const agent = createDriverHttpAgent();
|
|
136
|
+
const wrappedFetch = createAgentBackedFetch(agent);
|
|
137
|
+
|
|
138
|
+
try {
|
|
139
|
+
const response = await wrappedFetch(new Request(server.url));
|
|
140
|
+
await expect(response.text()).resolves.toBe('ok');
|
|
141
|
+
} finally {
|
|
142
|
+
await agent.close();
|
|
143
|
+
await server.close();
|
|
144
|
+
}
|
|
145
|
+
});
|
|
146
|
+
|
|
147
|
+
it('preserves a global Request body when callers pass undefined init fields', async () => {
|
|
148
|
+
const server = await startServer((req, res) => {
|
|
149
|
+
let body = '';
|
|
150
|
+
req.setEncoding('utf8');
|
|
151
|
+
req.on('data', (chunk) => {
|
|
152
|
+
body += chunk;
|
|
153
|
+
});
|
|
154
|
+
req.on('end', () => {
|
|
155
|
+
res.writeHead(200, { 'content-type': 'application/json' });
|
|
156
|
+
res.end(
|
|
157
|
+
JSON.stringify({
|
|
158
|
+
method: req.method,
|
|
159
|
+
body,
|
|
160
|
+
contentType: req.headers['content-type'],
|
|
161
|
+
}),
|
|
162
|
+
);
|
|
163
|
+
});
|
|
164
|
+
});
|
|
165
|
+
const agent = createDriverHttpAgent();
|
|
166
|
+
const wrappedFetch = createAgentBackedFetch(agent);
|
|
167
|
+
const request = new Request(server.url, {
|
|
168
|
+
method: 'POST',
|
|
169
|
+
headers: { 'content-type': 'application/json' },
|
|
170
|
+
body: JSON.stringify({ ok: true }),
|
|
171
|
+
});
|
|
172
|
+
|
|
173
|
+
try {
|
|
174
|
+
const response = await wrappedFetch(request, {
|
|
175
|
+
method: undefined,
|
|
176
|
+
body: undefined,
|
|
177
|
+
});
|
|
178
|
+
await expect(response.json()).resolves.toEqual({
|
|
179
|
+
method: 'POST',
|
|
180
|
+
body: '{"ok":true}',
|
|
181
|
+
contentType: 'application/json',
|
|
182
|
+
});
|
|
183
|
+
} finally {
|
|
184
|
+
await agent.close();
|
|
185
|
+
await server.close();
|
|
186
|
+
}
|
|
187
|
+
});
|
|
188
|
+
|
|
189
|
+
it('enforces headersTimeout through the wrapped fetch', async () => {
|
|
190
|
+
const server = await startServer((_req, res) => {
|
|
191
|
+
const timer = setTimeout(() => {
|
|
192
|
+
if (!res.destroyed) {
|
|
193
|
+
res.writeHead(200, { 'content-type': 'text/plain' });
|
|
194
|
+
res.end('late');
|
|
195
|
+
}
|
|
196
|
+
}, 3_000);
|
|
197
|
+
res.on('close', () => clearTimeout(timer));
|
|
198
|
+
});
|
|
199
|
+
const agent = createDriverHttpAgent({ headersTimeout: 100 });
|
|
200
|
+
const wrappedFetch = createAgentBackedFetch(agent);
|
|
201
|
+
const startedAt = Date.now();
|
|
202
|
+
|
|
203
|
+
try {
|
|
204
|
+
await expect(wrappedFetch(server.url)).rejects.toThrow();
|
|
205
|
+
expect(Date.now() - startedAt).toBeLessThan(2_500);
|
|
206
|
+
} finally {
|
|
207
|
+
await agent.close();
|
|
208
|
+
await server.close();
|
|
209
|
+
}
|
|
210
|
+
});
|
|
211
|
+
|
|
212
|
+
it('enforces bodyTimeout when a response body stalls', async () => {
|
|
213
|
+
const server = await startServer((_req, res) => {
|
|
214
|
+
res.writeHead(200, { 'content-type': 'text/plain' });
|
|
215
|
+
res.write('first');
|
|
216
|
+
const timer = setTimeout(() => {
|
|
217
|
+
if (!res.destroyed) {
|
|
218
|
+
res.end('second');
|
|
219
|
+
}
|
|
220
|
+
}, 3_000);
|
|
221
|
+
res.on('close', () => clearTimeout(timer));
|
|
222
|
+
});
|
|
223
|
+
const agent = createDriverHttpAgent({ bodyTimeout: 100 });
|
|
224
|
+
const wrappedFetch = createAgentBackedFetch(agent);
|
|
225
|
+
const startedAt = Date.now();
|
|
226
|
+
|
|
227
|
+
try {
|
|
228
|
+
const response = await wrappedFetch(server.url);
|
|
229
|
+
await expect(response.text()).rejects.toThrow();
|
|
230
|
+
expect(Date.now() - startedAt).toBeLessThan(2_500);
|
|
231
|
+
} finally {
|
|
232
|
+
await agent.close();
|
|
233
|
+
await server.close();
|
|
234
|
+
}
|
|
235
|
+
});
|
|
236
|
+
|
|
237
|
+
it('reuses and closes the lazy driver HTTP agent', () => {
|
|
238
|
+
const driver = new TestDriver({});
|
|
239
|
+
const agent = driver.getAgent();
|
|
240
|
+
const closeSpy = vi.spyOn(agent, 'close').mockResolvedValue();
|
|
241
|
+
|
|
242
|
+
expect(driver.getAgent()).toBe(agent);
|
|
243
|
+
expect(driver.getFetch()).toBe(driver.getFetch());
|
|
244
|
+
|
|
245
|
+
driver.destroy();
|
|
246
|
+
|
|
247
|
+
expect(closeSpy).toHaveBeenCalledTimes(1);
|
|
248
|
+
expect(driver.getAgent()).not.toBe(agent);
|
|
249
|
+
|
|
250
|
+
driver.destroy();
|
|
251
|
+
});
|
|
252
|
+
|
|
253
|
+
it('applies per-execution timeout overrides without replacing the cached driver fetch', async () => {
|
|
254
|
+
const server = await startServer((_req, res) => {
|
|
255
|
+
const timer = setTimeout(() => {
|
|
256
|
+
if (!res.destroyed) {
|
|
257
|
+
res.writeHead(200, { 'content-type': 'text/plain' });
|
|
258
|
+
res.end('late');
|
|
259
|
+
}
|
|
260
|
+
}, 3_000);
|
|
261
|
+
res.on('close', () => clearTimeout(timer));
|
|
262
|
+
});
|
|
263
|
+
const driver = new TestDriver({ httpTimeout: { headersTimeout: 2_000 } });
|
|
264
|
+
const cachedFetch = driver.getFetch();
|
|
265
|
+
const startedAt = Date.now();
|
|
266
|
+
|
|
267
|
+
try {
|
|
268
|
+
await expect(
|
|
269
|
+
driver.execute([{ role: PromptRole.user, content: server.url }], {
|
|
270
|
+
model: 'test-model',
|
|
271
|
+
httpTimeout: { headersTimeout: 100 },
|
|
272
|
+
}),
|
|
273
|
+
).rejects.toThrow();
|
|
274
|
+
expect(Date.now() - startedAt).toBeLessThan(2_500);
|
|
275
|
+
expect(driver.getFetch()).toBe(cachedFetch);
|
|
276
|
+
} finally {
|
|
277
|
+
driver.destroy();
|
|
278
|
+
await server.close();
|
|
279
|
+
}
|
|
280
|
+
});
|
|
281
|
+
});
|
|
@@ -0,0 +1,177 @@
|
|
|
1
|
+
import { AsyncLocalStorage } from 'node:async_hooks';
|
|
2
|
+
import type { HttpTimeoutOptions } from '@llumiverse/common';
|
|
3
|
+
import { Agent } from 'undici';
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Default HTTP timeouts used by {@link createDriverHttpAgent} when the
|
|
7
|
+
* caller does not override them. These are deliberately tighter than
|
|
8
|
+
* Node's undici default (5 minutes for headers and body) so a hung
|
|
9
|
+
* upstream LLM-provider call surfaces in seconds rather than blocking
|
|
10
|
+
* the whole request budget.
|
|
11
|
+
*
|
|
12
|
+
* Drivers with workloads that have legitimate long pauses (image
|
|
13
|
+
* generation, tool-using agent streams) should pass higher overrides
|
|
14
|
+
* via {@link HttpTimeoutOptions} rather than relying on the defaults.
|
|
15
|
+
*/
|
|
16
|
+
export const DEFAULT_DRIVER_HTTP_TIMEOUTS: Required<HttpTimeoutOptions> = {
|
|
17
|
+
headersTimeout: 60_000,
|
|
18
|
+
bodyTimeout: 60_000,
|
|
19
|
+
connectTimeout: 10_000,
|
|
20
|
+
keepAliveTimeout: 30_000,
|
|
21
|
+
};
|
|
22
|
+
|
|
23
|
+
const scopedHttpAgent = new AsyncLocalStorage<Agent>();
|
|
24
|
+
|
|
25
|
+
export function resolveDriverHttpTimeouts(opts?: HttpTimeoutOptions): Required<HttpTimeoutOptions> {
|
|
26
|
+
return {
|
|
27
|
+
headersTimeout: opts?.headersTimeout ?? DEFAULT_DRIVER_HTTP_TIMEOUTS.headersTimeout,
|
|
28
|
+
bodyTimeout: opts?.bodyTimeout ?? DEFAULT_DRIVER_HTTP_TIMEOUTS.bodyTimeout,
|
|
29
|
+
connectTimeout: opts?.connectTimeout ?? DEFAULT_DRIVER_HTTP_TIMEOUTS.connectTimeout,
|
|
30
|
+
keepAliveTimeout: opts?.keepAliveTimeout ?? DEFAULT_DRIVER_HTTP_TIMEOUTS.keepAliveTimeout,
|
|
31
|
+
};
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
export function mergeDriverHttpTimeoutOptions(
|
|
35
|
+
defaults?: HttpTimeoutOptions,
|
|
36
|
+
override?: HttpTimeoutOptions,
|
|
37
|
+
): HttpTimeoutOptions | undefined {
|
|
38
|
+
if (!override) {
|
|
39
|
+
return defaults;
|
|
40
|
+
}
|
|
41
|
+
return {
|
|
42
|
+
...(defaults ?? {}),
|
|
43
|
+
...stripUndefinedHttpTimeoutOptions(override),
|
|
44
|
+
};
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
/**
|
|
48
|
+
* Build an undici `Agent` configured from {@link HttpTimeoutOptions},
|
|
49
|
+
* falling back to {@link DEFAULT_DRIVER_HTTP_TIMEOUTS} for unset fields.
|
|
50
|
+
*
|
|
51
|
+
* The Agent pools sockets — reuse it for the lifetime of the driver
|
|
52
|
+
* and close it on `destroy()`.
|
|
53
|
+
*
|
|
54
|
+
* Node-only. `@llumiverse/drivers` is itself Node-only in practice
|
|
55
|
+
* because vertexai/bedrock pull `@google-cloud/*` / `@aws-sdk/*`, so
|
|
56
|
+
* depending on undici from core is acceptable.
|
|
57
|
+
*/
|
|
58
|
+
export function createDriverHttpAgent(opts?: HttpTimeoutOptions): Agent {
|
|
59
|
+
const timeouts = resolveDriverHttpTimeouts(opts);
|
|
60
|
+
return new Agent({
|
|
61
|
+
headersTimeout: timeouts.headersTimeout,
|
|
62
|
+
bodyTimeout: timeouts.bodyTimeout,
|
|
63
|
+
connectTimeout: timeouts.connectTimeout,
|
|
64
|
+
keepAliveTimeout: timeouts.keepAliveTimeout,
|
|
65
|
+
});
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
/**
|
|
69
|
+
* Wrap the global `fetch` so every request routes through the given Agent.
|
|
70
|
+
* The returned function is type-compatible with global `fetch`, so it
|
|
71
|
+
* can be passed directly to SDKs that accept a `fetch` option (OpenAI,
|
|
72
|
+
* Anthropic, `@google/genai`, Bedrock via Smithy, …) or used as a
|
|
73
|
+
* drop-in replacement for global `fetch` in drivers that make raw HTTP
|
|
74
|
+
* calls.
|
|
75
|
+
*
|
|
76
|
+
* `@vertesia/api-fetch-client` builds requests with `globalThis.Request`,
|
|
77
|
+
* while the undici package uses its own `Request` class. Calling
|
|
78
|
+
* `undici.fetch` directly can parse a global Request as `"[object Request]"`.
|
|
79
|
+
* Use `globalThis.fetch` (undici-backed in Node/Bun) and pass the undici
|
|
80
|
+
* dispatcher extension through init so the Agent timeout behavior is still
|
|
81
|
+
* applied without crossing Request/Response implementations.
|
|
82
|
+
*/
|
|
83
|
+
export function createAgentBackedFetch(agent: Agent): typeof fetch {
|
|
84
|
+
return ((input: RequestInfo | URL, init?: RequestInit) => {
|
|
85
|
+
const requestInput =
|
|
86
|
+
typeof Request !== 'undefined' && input instanceof Request
|
|
87
|
+
? normalizeRequestInput(input, init)
|
|
88
|
+
: { input, init };
|
|
89
|
+
|
|
90
|
+
const dispatcher = scopedHttpAgent.getStore() ?? agent;
|
|
91
|
+
return globalThis.fetch(requestInput.input, {
|
|
92
|
+
...(requestInput.init ?? {}),
|
|
93
|
+
dispatcher,
|
|
94
|
+
} as RequestInit & { dispatcher?: unknown });
|
|
95
|
+
}) as unknown as typeof fetch;
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
export interface DriverHttpAgentScope {
|
|
99
|
+
run<T>(callback: () => T): T;
|
|
100
|
+
close(): Promise<void>;
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
const NOOP_HTTP_AGENT_SCOPE: DriverHttpAgentScope = {
|
|
104
|
+
run: <T>(callback: () => T): T => callback(),
|
|
105
|
+
close: () => Promise.resolve(),
|
|
106
|
+
};
|
|
107
|
+
|
|
108
|
+
export function createDriverHttpAgentScope(
|
|
109
|
+
defaults?: HttpTimeoutOptions,
|
|
110
|
+
override?: HttpTimeoutOptions,
|
|
111
|
+
): DriverHttpAgentScope {
|
|
112
|
+
if (!override) {
|
|
113
|
+
return NOOP_HTTP_AGENT_SCOPE;
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
const agent = createDriverHttpAgent(mergeDriverHttpTimeoutOptions(defaults, override));
|
|
117
|
+
return {
|
|
118
|
+
run: <T>(callback: () => T): T => scopedHttpAgent.run(agent, callback),
|
|
119
|
+
close: async () => {
|
|
120
|
+
await agent.close().catch(() => {
|
|
121
|
+
/* shutdown best-effort */
|
|
122
|
+
});
|
|
123
|
+
},
|
|
124
|
+
};
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
type NormalizedRequestInput = {
|
|
128
|
+
input: RequestInfo | URL;
|
|
129
|
+
init?: RequestInit & { duplex?: 'half' };
|
|
130
|
+
};
|
|
131
|
+
|
|
132
|
+
function normalizeRequestInput(request: Request, init?: RequestInit): NormalizedRequestInput {
|
|
133
|
+
const overrides = stripUndefinedRequestInit(init);
|
|
134
|
+
const requestInit: RequestInit & { duplex?: 'half' } = {
|
|
135
|
+
method: request.method,
|
|
136
|
+
headers: request.headers,
|
|
137
|
+
body: request.body,
|
|
138
|
+
signal: request.signal,
|
|
139
|
+
cache: request.cache,
|
|
140
|
+
credentials: request.credentials,
|
|
141
|
+
integrity: request.integrity,
|
|
142
|
+
keepalive: request.keepalive,
|
|
143
|
+
mode: request.mode,
|
|
144
|
+
redirect: request.redirect,
|
|
145
|
+
referrer: request.referrer,
|
|
146
|
+
referrerPolicy: request.referrerPolicy,
|
|
147
|
+
};
|
|
148
|
+
|
|
149
|
+
if (request.body) {
|
|
150
|
+
requestInit.duplex = 'half';
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
return {
|
|
154
|
+
input: request.url,
|
|
155
|
+
init: {
|
|
156
|
+
...requestInit,
|
|
157
|
+
...overrides,
|
|
158
|
+
},
|
|
159
|
+
};
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
function stripUndefinedRequestInit(init?: RequestInit): RequestInit | undefined {
|
|
163
|
+
if (!init) {
|
|
164
|
+
return undefined;
|
|
165
|
+
}
|
|
166
|
+
const entries = Object.entries(init).filter(([, value]) => value !== undefined);
|
|
167
|
+
return Object.fromEntries(entries) as RequestInit;
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
function stripUndefinedHttpTimeoutOptions(opts: HttpTimeoutOptions): HttpTimeoutOptions {
|
|
171
|
+
const entries = Object.entries(opts).filter(([, value]) => value !== undefined);
|
|
172
|
+
return Object.fromEntries(entries) as HttpTimeoutOptions;
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
/** Re-export the undici `Agent` type so driver code can type its agent
|
|
176
|
+
* field without adding an extra undici import. */
|
|
177
|
+
export type { Agent as DriverHttpAgent } from 'undici';
|
package/src/index.ts
CHANGED
|
@@ -1,5 +1,7 @@
|
|
|
1
|
-
export * from
|
|
2
|
-
export * from
|
|
3
|
-
export
|
|
4
|
-
export * from
|
|
5
|
-
export * from
|
|
1
|
+
export * from '@llumiverse/common';
|
|
2
|
+
export * from './conversation-utils.js';
|
|
3
|
+
export type { AbstractDriver, Driver } from './Driver.js';
|
|
4
|
+
export * from './embeddings.js';
|
|
5
|
+
export * from './json.js';
|
|
6
|
+
export * from './logger.js';
|
|
7
|
+
export * from './stream.js';
|
package/src/json.ts
CHANGED
|
@@ -1,9 +1,9 @@
|
|
|
1
|
-
import { JSONValue } from
|
|
1
|
+
import type { JSONValue } from '@llumiverse/common';
|
|
2
2
|
import { jsonrepair } from 'jsonrepair';
|
|
3
3
|
|
|
4
4
|
function extractJsonFromText(text: string): string {
|
|
5
|
-
const start = text.indexOf(
|
|
6
|
-
const end = text.lastIndexOf(
|
|
5
|
+
const start = text.indexOf('{');
|
|
6
|
+
const end = text.lastIndexOf('}');
|
|
7
7
|
return text.substring(start, end + 1);
|
|
8
8
|
}
|
|
9
9
|
|
|
@@ -15,11 +15,12 @@ export function parseJSON(text: string): JSONValue {
|
|
|
15
15
|
text = text.trim();
|
|
16
16
|
try {
|
|
17
17
|
return JSON.parse(text);
|
|
18
|
-
} catch (err:
|
|
18
|
+
} catch (err: unknown) {
|
|
19
19
|
// use a relaxed parser
|
|
20
20
|
try {
|
|
21
21
|
return JSON.parse(jsonrepair(text));
|
|
22
|
-
} catch
|
|
22
|
+
} catch {
|
|
23
|
+
// throw the original error
|
|
23
24
|
throw err;
|
|
24
25
|
}
|
|
25
26
|
}
|
package/src/logger.ts
ADDED
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
import type { Logger } from '@llumiverse/common';
|
|
2
|
+
|
|
3
|
+
// Helper to create logger methods that support both message-only and object-first signatures.
|
|
4
|
+
function createConsoleLoggerMethod(consoleMethod: (...args: unknown[]) => void): Logger['info'] {
|
|
5
|
+
return ((objOrMsg: unknown, msgOrNever?: string, ...args: (string | number | boolean)[]) => {
|
|
6
|
+
if (typeof objOrMsg === 'string') {
|
|
7
|
+
// Message-only: logger.info("message", ...args)
|
|
8
|
+
consoleMethod(objOrMsg, msgOrNever, ...args);
|
|
9
|
+
} else if (msgOrNever !== undefined) {
|
|
10
|
+
// Object-first: logger.info({ obj }, "message", ...args)
|
|
11
|
+
consoleMethod(msgOrNever, objOrMsg, ...args);
|
|
12
|
+
} else {
|
|
13
|
+
// Object-only: logger.info({ obj })
|
|
14
|
+
consoleMethod(objOrMsg, ...args);
|
|
15
|
+
}
|
|
16
|
+
}) as Logger['info'];
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
const ConsoleLogger: Logger = {
|
|
20
|
+
debug: createConsoleLoggerMethod(console.debug.bind(console)),
|
|
21
|
+
info: createConsoleLoggerMethod(console.info.bind(console)),
|
|
22
|
+
warn: createConsoleLoggerMethod(console.warn.bind(console)),
|
|
23
|
+
error: createConsoleLoggerMethod(console.error.bind(console)),
|
|
24
|
+
};
|
|
25
|
+
|
|
26
|
+
const noop = () => void 0;
|
|
27
|
+
const NoopLogger: Logger = {
|
|
28
|
+
debug: noop as Logger['debug'],
|
|
29
|
+
info: noop as Logger['info'],
|
|
30
|
+
warn: noop as Logger['warn'],
|
|
31
|
+
error: noop as Logger['error'],
|
|
32
|
+
};
|
|
33
|
+
|
|
34
|
+
export function createLogger(logger: Logger | 'console' | undefined) {
|
|
35
|
+
if (logger === 'console') {
|
|
36
|
+
return ConsoleLogger;
|
|
37
|
+
} else if (logger) {
|
|
38
|
+
return logger;
|
|
39
|
+
} else {
|
|
40
|
+
return NoopLogger;
|
|
41
|
+
}
|
|
42
|
+
}
|
package/src/resolver.ts
CHANGED
|
@@ -6,31 +6,34 @@
|
|
|
6
6
|
* @param name the name of the property.
|
|
7
7
|
* @returns the property value
|
|
8
8
|
*/
|
|
9
|
-
function _prop(object:
|
|
10
|
-
if (object === undefined) {
|
|
9
|
+
function _prop(object: unknown, name: string): unknown {
|
|
10
|
+
if (object === undefined || object === null) {
|
|
11
11
|
return undefined;
|
|
12
12
|
}
|
|
13
13
|
if (Array.isArray(object)) {
|
|
14
14
|
const index = +name;
|
|
15
|
-
if (isNaN(index)) {
|
|
15
|
+
if (Number.isNaN(index)) {
|
|
16
16
|
// map array to property
|
|
17
|
-
return object.map(item =>
|
|
17
|
+
return object.map((item) =>
|
|
18
|
+
item && typeof item === 'object' ? (item as Record<string, unknown>)[name] : undefined,
|
|
19
|
+
);
|
|
18
20
|
} else {
|
|
19
21
|
return object[index];
|
|
20
22
|
}
|
|
23
|
+
} else if (typeof object === 'object') {
|
|
24
|
+
return (object as Record<string, unknown>)[name];
|
|
21
25
|
} else {
|
|
22
|
-
return
|
|
26
|
+
return undefined;
|
|
23
27
|
}
|
|
24
|
-
|
|
25
28
|
}
|
|
26
29
|
|
|
27
|
-
export function resolveField(object:
|
|
28
|
-
let p = object
|
|
30
|
+
export function resolveField(object: unknown, path: string[]): unknown {
|
|
31
|
+
let p: unknown = object;
|
|
29
32
|
if (!p) return p;
|
|
30
33
|
if (!path.length) return p;
|
|
31
34
|
const last = path.length - 1;
|
|
32
35
|
for (let i = 0; i < last; i++) {
|
|
33
|
-
p = _prop(p, path[i])
|
|
36
|
+
p = _prop(p, path[i]);
|
|
34
37
|
if (!p) {
|
|
35
38
|
return undefined;
|
|
36
39
|
}
|
package/src/stream.ts
CHANGED
|
@@ -1,4 +1,3 @@
|
|
|
1
|
-
|
|
2
1
|
export async function readStreamAsBase64(stream: ReadableStream): Promise<string> {
|
|
3
2
|
const uint8Array = await readStreamAsUint8Array(stream);
|
|
4
3
|
return Buffer.from(uint8Array).toString('base64');
|
|
@@ -12,20 +11,20 @@ export async function readStreamAsString(stream: ReadableStream): Promise<string
|
|
|
12
11
|
export async function readStreamAsUint8Array(stream: ReadableStream): Promise<Uint8Array> {
|
|
13
12
|
const chunks: Uint8Array[] = [];
|
|
14
13
|
let totalLength = 0;
|
|
15
|
-
|
|
14
|
+
|
|
16
15
|
for await (const chunk of stream) {
|
|
17
16
|
const uint8Chunk = chunk instanceof Uint8Array ? chunk : new Uint8Array(chunk);
|
|
18
|
-
|
|
17
|
+
|
|
19
18
|
chunks.push(uint8Chunk);
|
|
20
19
|
totalLength += uint8Chunk.length;
|
|
21
20
|
}
|
|
22
|
-
|
|
21
|
+
|
|
23
22
|
const combined = new Uint8Array(totalLength);
|
|
24
23
|
let offset = 0;
|
|
25
24
|
for (const chunk of chunks) {
|
|
26
25
|
combined.set(chunk, offset);
|
|
27
26
|
offset += chunk.length;
|
|
28
27
|
}
|
|
29
|
-
|
|
28
|
+
|
|
30
29
|
return combined;
|
|
31
30
|
}
|