@namzu/sdk 1.2.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 +12 -0
- package/dist/provider/__tests__/registry.test.d.ts +5 -0
- package/dist/provider/__tests__/registry.test.d.ts.map +1 -1
- package/dist/provider/__tests__/registry.test.js +186 -1
- package/dist/provider/__tests__/registry.test.js.map +1 -1
- package/dist/provider/index.d.ts +1 -1
- package/dist/provider/index.d.ts.map +1 -1
- package/dist/provider/index.js +1 -1
- package/dist/provider/index.js.map +1 -1
- package/dist/provider/registry.d.ts +81 -1
- package/dist/provider/registry.d.ts.map +1 -1
- package/dist/provider/registry.js +173 -7
- package/dist/provider/registry.js.map +1 -1
- package/dist/public-runtime.d.ts +1 -1
- package/dist/public-runtime.d.ts.map +1 -1
- package/dist/public-runtime.js +1 -1
- package/dist/public-runtime.js.map +1 -1
- package/dist/types/provider/config.d.ts +33 -0
- package/dist/types/provider/config.d.ts.map +1 -1
- package/dist/types/provider/index.d.ts +1 -1
- package/dist/types/provider/index.d.ts.map +1 -1
- package/package.json +1 -1
- package/src/provider/__tests__/registry.test.ts +266 -1
- package/src/provider/index.ts +7 -1
- package/src/provider/registry.ts +202 -7
- package/src/public-runtime.ts +2 -0
- package/src/types/provider/config.ts +36 -0
- package/src/types/provider/index.ts +3 -0
|
@@ -1,9 +1,16 @@
|
|
|
1
1
|
import { beforeEach, describe, expect, it } from 'vitest'
|
|
2
|
-
import type {
|
|
2
|
+
import type {
|
|
3
|
+
LLMProvider,
|
|
4
|
+
LazyProviderModule,
|
|
5
|
+
ProviderCapabilities,
|
|
6
|
+
} from '../../types/provider/index.js'
|
|
7
|
+
import { PERMISSIVE_PROVIDER_CAPABILITIES, resolveProviderCapabilities } from '../capabilities.js'
|
|
3
8
|
import { registerMock } from '../mock-register.js'
|
|
4
9
|
import { MockLLMProvider } from '../mock.js'
|
|
5
10
|
import {
|
|
6
11
|
DuplicateProviderError,
|
|
12
|
+
LazyProviderLoadError,
|
|
13
|
+
LazyProviderSyncCreateError,
|
|
7
14
|
ProviderRegistry,
|
|
8
15
|
UnknownProviderError,
|
|
9
16
|
__resetProviderRegistryInternal,
|
|
@@ -16,9 +23,15 @@ interface TestProviderConfig {
|
|
|
16
23
|
value?: string
|
|
17
24
|
}
|
|
18
25
|
|
|
26
|
+
interface LazyTestProviderConfig {
|
|
27
|
+
type: 'lazytest'
|
|
28
|
+
value?: string
|
|
29
|
+
}
|
|
30
|
+
|
|
19
31
|
declare module '../../types/provider/config.js' {
|
|
20
32
|
interface ProviderConfigRegistry {
|
|
21
33
|
test: TestProviderConfig
|
|
34
|
+
lazytest: LazyTestProviderConfig
|
|
22
35
|
}
|
|
23
36
|
}
|
|
24
37
|
|
|
@@ -52,6 +65,41 @@ const TEST_CAPS: ProviderCapabilities = {
|
|
|
52
65
|
supportsFunctionCalling: false,
|
|
53
66
|
}
|
|
54
67
|
|
|
68
|
+
class LazyTestProvider implements LLMProvider {
|
|
69
|
+
readonly id = 'lazytest'
|
|
70
|
+
readonly name = 'Lazy Test'
|
|
71
|
+
constructor(
|
|
72
|
+
public readonly config: LazyTestProviderConfig,
|
|
73
|
+
readonly capabilities?: ProviderCapabilities,
|
|
74
|
+
) {}
|
|
75
|
+
async *chatStream() {
|
|
76
|
+
yield { id: 'x', delta: { content: 'ok' } }
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
const HINT_CAPS: ProviderCapabilities = {
|
|
81
|
+
supportsTools: false,
|
|
82
|
+
supportsStreaming: false,
|
|
83
|
+
supportsFunctionCalling: false,
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
const MODULE_CAPS: ProviderCapabilities = {
|
|
87
|
+
supportsTools: true,
|
|
88
|
+
supportsStreaming: true,
|
|
89
|
+
supportsFunctionCalling: true,
|
|
90
|
+
supportsVision: false,
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
function deferred<T>() {
|
|
94
|
+
let resolve!: (value: T) => void
|
|
95
|
+
let reject!: (reason?: unknown) => void
|
|
96
|
+
const promise = new Promise<T>((res, rej) => {
|
|
97
|
+
resolve = res
|
|
98
|
+
reject = rej
|
|
99
|
+
})
|
|
100
|
+
return { promise, resolve, reject }
|
|
101
|
+
}
|
|
102
|
+
|
|
55
103
|
describe('ProviderRegistry', () => {
|
|
56
104
|
beforeEach(() => {
|
|
57
105
|
__resetProviderRegistryInternal()
|
|
@@ -152,4 +200,221 @@ describe('ProviderRegistry', () => {
|
|
|
152
200
|
expect(err.name).toBe('DuplicateProviderError')
|
|
153
201
|
})
|
|
154
202
|
})
|
|
203
|
+
|
|
204
|
+
describe('registerLazy', () => {
|
|
205
|
+
it('does not invoke the loader at registration time', () => {
|
|
206
|
+
let calls = 0
|
|
207
|
+
ProviderRegistry.registerLazy('lazytest', async () => {
|
|
208
|
+
calls++
|
|
209
|
+
return { create: (config) => new LazyTestProvider(config) }
|
|
210
|
+
})
|
|
211
|
+
|
|
212
|
+
expect(calls).toBe(0)
|
|
213
|
+
expect(ProviderRegistry.isSupported('lazytest')).toBe(true)
|
|
214
|
+
expect(ProviderRegistry.listTypes()).toContain('lazytest')
|
|
215
|
+
})
|
|
216
|
+
|
|
217
|
+
it('throws DuplicateProviderError over an existing eager registration', () => {
|
|
218
|
+
ProviderRegistry.register('test', TestProvider, TEST_CAPS)
|
|
219
|
+
expect(() =>
|
|
220
|
+
ProviderRegistry.registerLazy('test', async () => ({
|
|
221
|
+
create: (config) => new TestProvider(config),
|
|
222
|
+
})),
|
|
223
|
+
).toThrowError(DuplicateProviderError)
|
|
224
|
+
})
|
|
225
|
+
|
|
226
|
+
it('throws DuplicateProviderError over an existing lazy registration', () => {
|
|
227
|
+
const loader = async (): Promise<LazyProviderModule<LazyTestProviderConfig>> => ({
|
|
228
|
+
create: (config) => new LazyTestProvider(config),
|
|
229
|
+
})
|
|
230
|
+
ProviderRegistry.registerLazy('lazytest', loader)
|
|
231
|
+
expect(() => ProviderRegistry.registerLazy('lazytest', loader)).toThrowError(
|
|
232
|
+
DuplicateProviderError,
|
|
233
|
+
)
|
|
234
|
+
})
|
|
235
|
+
|
|
236
|
+
it('replace: true swaps between eager and lazy registrations', async () => {
|
|
237
|
+
ProviderRegistry.register('lazytest', LazyTestProvider, TEST_CAPS)
|
|
238
|
+
ProviderRegistry.registerLazy(
|
|
239
|
+
'lazytest',
|
|
240
|
+
async () => ({ create: (config) => new LazyTestProvider(config) }),
|
|
241
|
+
{ replace: true },
|
|
242
|
+
)
|
|
243
|
+
expect(() => ProviderRegistry.createProvider({ type: 'lazytest' })).toThrowError(
|
|
244
|
+
LazyProviderSyncCreateError,
|
|
245
|
+
)
|
|
246
|
+
|
|
247
|
+
ProviderRegistry.register('lazytest', LazyTestProvider, TEST_CAPS, { replace: true })
|
|
248
|
+
expect(ProviderRegistry.createProvider({ type: 'lazytest' })).toBeInstanceOf(LazyTestProvider)
|
|
249
|
+
})
|
|
250
|
+
|
|
251
|
+
it('sync create()/createProvider() always throws for lazy types, even after load', async () => {
|
|
252
|
+
ProviderRegistry.registerLazy('lazytest', async () => ({
|
|
253
|
+
create: (config) => new LazyTestProvider(config),
|
|
254
|
+
}))
|
|
255
|
+
|
|
256
|
+
expect(() => ProviderRegistry.create({ type: 'lazytest' })).toThrowError(
|
|
257
|
+
LazyProviderSyncCreateError,
|
|
258
|
+
)
|
|
259
|
+
|
|
260
|
+
await ProviderRegistry.createAsync({ type: 'lazytest' })
|
|
261
|
+
expect(() => ProviderRegistry.createProvider({ type: 'lazytest' })).toThrowError(
|
|
262
|
+
LazyProviderSyncCreateError,
|
|
263
|
+
)
|
|
264
|
+
})
|
|
265
|
+
|
|
266
|
+
it('unregister removes a lazy registration', () => {
|
|
267
|
+
ProviderRegistry.registerLazy('lazytest', async () => ({
|
|
268
|
+
create: (config) => new LazyTestProvider(config),
|
|
269
|
+
}))
|
|
270
|
+
expect(ProviderRegistry.unregister('lazytest')).toBe(true)
|
|
271
|
+
expect(ProviderRegistry.isSupported('lazytest')).toBe(false)
|
|
272
|
+
})
|
|
273
|
+
})
|
|
274
|
+
|
|
275
|
+
describe('createAsync', () => {
|
|
276
|
+
it('first create invokes the loader once; subsequent creates reuse the cached factory', async () => {
|
|
277
|
+
let calls = 0
|
|
278
|
+
ProviderRegistry.registerLazy('lazytest', async () => {
|
|
279
|
+
calls++
|
|
280
|
+
return { create: (config) => new LazyTestProvider(config) }
|
|
281
|
+
})
|
|
282
|
+
|
|
283
|
+
const first = await ProviderRegistry.createAsync({ type: 'lazytest', value: 'a' })
|
|
284
|
+
const second = await ProviderRegistry.createAsync({ type: 'lazytest', value: 'b' })
|
|
285
|
+
|
|
286
|
+
expect(calls).toBe(1)
|
|
287
|
+
expect(first.provider).toBeInstanceOf(LazyTestProvider)
|
|
288
|
+
expect(second.provider).toBeInstanceOf(LazyTestProvider)
|
|
289
|
+
expect((second.provider as LazyTestProvider).config.value).toBe('b')
|
|
290
|
+
})
|
|
291
|
+
|
|
292
|
+
it('concurrent first-creates share a single loader invocation', async () => {
|
|
293
|
+
let calls = 0
|
|
294
|
+
const gate = deferred<void>()
|
|
295
|
+
ProviderRegistry.registerLazy('lazytest', async () => {
|
|
296
|
+
calls++
|
|
297
|
+
await gate.promise
|
|
298
|
+
return { create: (config) => new LazyTestProvider(config) }
|
|
299
|
+
})
|
|
300
|
+
|
|
301
|
+
const a = ProviderRegistry.createAsync({ type: 'lazytest' })
|
|
302
|
+
const b = ProviderRegistry.createAsync({ type: 'lazytest' })
|
|
303
|
+
gate.resolve()
|
|
304
|
+
|
|
305
|
+
const [ra, rb] = await Promise.all([a, b])
|
|
306
|
+
expect(calls).toBe(1)
|
|
307
|
+
expect(ra.provider).toBeInstanceOf(LazyTestProvider)
|
|
308
|
+
expect(rb.provider).toBeInstanceOf(LazyTestProvider)
|
|
309
|
+
})
|
|
310
|
+
|
|
311
|
+
it('loader rejection surfaces as LazyProviderLoadError and the next create retries', async () => {
|
|
312
|
+
let calls = 0
|
|
313
|
+
ProviderRegistry.registerLazy('lazytest', async () => {
|
|
314
|
+
calls++
|
|
315
|
+
if (calls === 1) {
|
|
316
|
+
throw new Error('transient network failure')
|
|
317
|
+
}
|
|
318
|
+
return { create: (config) => new LazyTestProvider(config) }
|
|
319
|
+
})
|
|
320
|
+
|
|
321
|
+
const failure = await ProviderRegistry.createAsync({ type: 'lazytest' }).catch(
|
|
322
|
+
(err: unknown) => err,
|
|
323
|
+
)
|
|
324
|
+
expect(failure).toBeInstanceOf(LazyProviderLoadError)
|
|
325
|
+
expect((failure as LazyProviderLoadError).providerType).toBe('lazytest')
|
|
326
|
+
expect(((failure as LazyProviderLoadError).cause as Error).message).toBe(
|
|
327
|
+
'transient network failure',
|
|
328
|
+
)
|
|
329
|
+
|
|
330
|
+
const { provider } = await ProviderRegistry.createAsync({ type: 'lazytest' })
|
|
331
|
+
expect(calls).toBe(2)
|
|
332
|
+
expect(provider).toBeInstanceOf(LazyTestProvider)
|
|
333
|
+
})
|
|
334
|
+
|
|
335
|
+
it('rejects with LazyProviderLoadError when the loader resolves an invalid module shape', async () => {
|
|
336
|
+
ProviderRegistry.registerLazy(
|
|
337
|
+
'lazytest',
|
|
338
|
+
async () => ({}) as unknown as LazyProviderModule<LazyTestProviderConfig>,
|
|
339
|
+
)
|
|
340
|
+
await expect(ProviderRegistry.createAsync({ type: 'lazytest' })).rejects.toThrowError(
|
|
341
|
+
LazyProviderLoadError,
|
|
342
|
+
)
|
|
343
|
+
})
|
|
344
|
+
|
|
345
|
+
it('works for eagerly registered types too', async () => {
|
|
346
|
+
const { provider, capabilities } = await ProviderRegistry.createAsync({
|
|
347
|
+
type: 'mock',
|
|
348
|
+
model: 'test-model',
|
|
349
|
+
})
|
|
350
|
+
expect(provider).toBeInstanceOf(MockLLMProvider)
|
|
351
|
+
expect(capabilities.supportsStreaming).toBe(true)
|
|
352
|
+
})
|
|
353
|
+
|
|
354
|
+
it('rejects with UnknownProviderError for unregistered types', async () => {
|
|
355
|
+
await expect(
|
|
356
|
+
ProviderRegistry.createAsync({ type: 'nonexistent' } as unknown as { type: 'mock' }),
|
|
357
|
+
).rejects.toThrowError(UnknownProviderError)
|
|
358
|
+
})
|
|
359
|
+
})
|
|
360
|
+
|
|
361
|
+
describe('lazy capabilities', () => {
|
|
362
|
+
it('answers with the registration hint before load, without invoking the loader', () => {
|
|
363
|
+
let calls = 0
|
|
364
|
+
ProviderRegistry.registerLazy(
|
|
365
|
+
'lazytest',
|
|
366
|
+
async () => {
|
|
367
|
+
calls++
|
|
368
|
+
return { create: (config) => new LazyTestProvider(config) }
|
|
369
|
+
},
|
|
370
|
+
{ capabilities: HINT_CAPS },
|
|
371
|
+
)
|
|
372
|
+
|
|
373
|
+
expect(ProviderRegistry.getCapabilities('lazytest')).toEqual(HINT_CAPS)
|
|
374
|
+
expect(calls).toBe(0)
|
|
375
|
+
})
|
|
376
|
+
|
|
377
|
+
it('answers permissively before load when no hint was given', () => {
|
|
378
|
+
ProviderRegistry.registerLazy('lazytest', async () => ({
|
|
379
|
+
create: (config) => new LazyTestProvider(config),
|
|
380
|
+
}))
|
|
381
|
+
expect(ProviderRegistry.getCapabilities('lazytest')).toEqual(PERMISSIVE_PROVIDER_CAPABILITIES)
|
|
382
|
+
})
|
|
383
|
+
|
|
384
|
+
it('module capabilities replace the hint after load', async () => {
|
|
385
|
+
ProviderRegistry.registerLazy(
|
|
386
|
+
'lazytest',
|
|
387
|
+
async () => ({
|
|
388
|
+
create: (config) => new LazyTestProvider(config),
|
|
389
|
+
capabilities: MODULE_CAPS,
|
|
390
|
+
}),
|
|
391
|
+
{ capabilities: HINT_CAPS },
|
|
392
|
+
)
|
|
393
|
+
|
|
394
|
+
const { capabilities } = await ProviderRegistry.createAsync({ type: 'lazytest' })
|
|
395
|
+
expect(capabilities).toEqual(MODULE_CAPS)
|
|
396
|
+
expect(ProviderRegistry.getCapabilities('lazytest')).toEqual(MODULE_CAPS)
|
|
397
|
+
})
|
|
398
|
+
|
|
399
|
+
it('the constructed instance own capabilities win at run time over any hint', async () => {
|
|
400
|
+
const instanceCaps: ProviderCapabilities = {
|
|
401
|
+
supportsTools: true,
|
|
402
|
+
supportsStreaming: true,
|
|
403
|
+
supportsFunctionCalling: false,
|
|
404
|
+
supportsVision: false,
|
|
405
|
+
}
|
|
406
|
+
ProviderRegistry.registerLazy(
|
|
407
|
+
'lazytest',
|
|
408
|
+
async () => ({ create: (config) => new LazyTestProvider(config, instanceCaps) }),
|
|
409
|
+
{ capabilities: HINT_CAPS },
|
|
410
|
+
)
|
|
411
|
+
|
|
412
|
+
const { provider } = await ProviderRegistry.createAsync({ type: 'lazytest' })
|
|
413
|
+
// The query runtime negotiates against the instance, not the registry:
|
|
414
|
+
const resolved = resolveProviderCapabilities(provider)
|
|
415
|
+
expect(resolved.supportsTools).toBe(true)
|
|
416
|
+
expect(resolved.supportsFunctionCalling).toBe(false)
|
|
417
|
+
expect(resolved.supportsVision).toBe(false)
|
|
418
|
+
})
|
|
419
|
+
})
|
|
155
420
|
})
|
package/src/provider/index.ts
CHANGED
|
@@ -1,4 +1,10 @@
|
|
|
1
|
-
export {
|
|
1
|
+
export {
|
|
2
|
+
ProviderRegistry,
|
|
3
|
+
UnknownProviderError,
|
|
4
|
+
DuplicateProviderError,
|
|
5
|
+
LazyProviderLoadError,
|
|
6
|
+
LazyProviderSyncCreateError,
|
|
7
|
+
} from './registry.js'
|
|
2
8
|
export { MockLLMProvider } from './mock.js'
|
|
3
9
|
export { registerMock, MOCK_CAPABILITIES } from './mock-register.js'
|
|
4
10
|
export {
|
package/src/provider/registry.ts
CHANGED
|
@@ -1,13 +1,17 @@
|
|
|
1
1
|
import type {
|
|
2
2
|
LLMProvider,
|
|
3
3
|
LLMProviderConstructor,
|
|
4
|
+
LazyProviderLoader,
|
|
5
|
+
LazyProviderModule,
|
|
4
6
|
ProviderCapabilities,
|
|
5
7
|
ProviderConfigRegistry,
|
|
6
8
|
ProviderFactoryConfig,
|
|
7
9
|
ProviderFactoryResult,
|
|
8
10
|
ProviderType,
|
|
11
|
+
RegisterLazyOptions,
|
|
9
12
|
RegisterOptions,
|
|
10
13
|
} from '../types/provider/index.js'
|
|
14
|
+
import { PERMISSIVE_PROVIDER_CAPABILITIES } from './capabilities.js'
|
|
11
15
|
|
|
12
16
|
export class UnknownProviderError extends Error {
|
|
13
17
|
readonly providerType: string
|
|
@@ -31,9 +35,99 @@ export class DuplicateProviderError extends Error {
|
|
|
31
35
|
}
|
|
32
36
|
}
|
|
33
37
|
|
|
38
|
+
/**
|
|
39
|
+
* The loader passed to `ProviderRegistry.registerLazy()` rejected (or
|
|
40
|
+
* resolved to something without a `create(config)` function). Wraps the
|
|
41
|
+
* original failure as `cause`. The failed load is NOT cached — the next
|
|
42
|
+
* `createAsync()` for the type re-invokes the loader, so a transient
|
|
43
|
+
* failure (network hiccup during a dynamic import) does not permanently
|
|
44
|
+
* poison the type.
|
|
45
|
+
*/
|
|
46
|
+
export class LazyProviderLoadError extends Error {
|
|
47
|
+
readonly providerType: string
|
|
48
|
+
|
|
49
|
+
constructor(providerType: string, cause: unknown) {
|
|
50
|
+
const detail = cause instanceof Error ? cause.message : String(cause)
|
|
51
|
+
super(`Failed to load lazy provider "${providerType}": ${detail}`, { cause })
|
|
52
|
+
this.name = 'LazyProviderLoadError'
|
|
53
|
+
this.providerType = providerType
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
/**
|
|
58
|
+
* A synchronous `create()`/`createProvider()` was called for a type
|
|
59
|
+
* registered via `registerLazy()`. Lazy types are only constructible
|
|
60
|
+
* through the async path — deterministically, even after the loader has
|
|
61
|
+
* resolved, so calling code never depends on load-order timing.
|
|
62
|
+
*/
|
|
63
|
+
export class LazyProviderSyncCreateError extends Error {
|
|
64
|
+
readonly providerType: string
|
|
65
|
+
|
|
66
|
+
constructor(providerType: string) {
|
|
67
|
+
super(
|
|
68
|
+
`Provider type "${providerType}" is registered lazily; synchronous create()/createProvider() cannot load it. Use ProviderRegistry.createAsync() or createProviderAsync().`,
|
|
69
|
+
)
|
|
70
|
+
this.name = 'LazyProviderSyncCreateError'
|
|
71
|
+
this.providerType = providerType
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
interface LazyProviderEntry {
|
|
76
|
+
loader: LazyProviderLoader<unknown>
|
|
77
|
+
/** In-flight load shared by concurrent first-creates (dedupe). */
|
|
78
|
+
loading?: Promise<LazyProviderModule<unknown>>
|
|
79
|
+
/** Cached module — set only on SUCCESS, so failures retry. */
|
|
80
|
+
module?: LazyProviderModule<unknown>
|
|
81
|
+
}
|
|
82
|
+
|
|
34
83
|
// Module-private state. Only the exported functions below can read/mutate.
|
|
35
84
|
const providers = new Map<string, LLMProviderConstructor<unknown>>()
|
|
36
85
|
const capabilities = new Map<string, ProviderCapabilities>()
|
|
86
|
+
const lazyProviders = new Map<string, LazyProviderEntry>()
|
|
87
|
+
|
|
88
|
+
async function loadLazyModule(
|
|
89
|
+
type: string,
|
|
90
|
+
entry: LazyProviderEntry,
|
|
91
|
+
): Promise<LazyProviderModule<unknown>> {
|
|
92
|
+
if (entry.module) {
|
|
93
|
+
return entry.module
|
|
94
|
+
}
|
|
95
|
+
let inflight = entry.loading
|
|
96
|
+
if (!inflight) {
|
|
97
|
+
// Promise.resolve().then(...) also catches loaders that throw synchronously.
|
|
98
|
+
inflight = Promise.resolve()
|
|
99
|
+
.then(() => entry.loader())
|
|
100
|
+
.then((mod) => {
|
|
101
|
+
if (typeof mod?.create !== 'function') {
|
|
102
|
+
throw new TypeError(
|
|
103
|
+
'loader resolved to a value without a create(config) function — map your dynamic import to { create: (config) => new Provider(config) }',
|
|
104
|
+
)
|
|
105
|
+
}
|
|
106
|
+
return mod
|
|
107
|
+
})
|
|
108
|
+
entry.loading = inflight
|
|
109
|
+
}
|
|
110
|
+
try {
|
|
111
|
+
const mod = await inflight
|
|
112
|
+
entry.module = mod
|
|
113
|
+
if (entry.loading === inflight) {
|
|
114
|
+
entry.loading = undefined
|
|
115
|
+
}
|
|
116
|
+
// The loaded module's declaration is authoritative at the type level:
|
|
117
|
+
// it replaces any registration-time hint for future getCapabilities().
|
|
118
|
+
if (mod.capabilities) {
|
|
119
|
+
capabilities.set(type, mod.capabilities)
|
|
120
|
+
}
|
|
121
|
+
return mod
|
|
122
|
+
} catch (cause) {
|
|
123
|
+
// Clear only OUR in-flight promise; a retry started by another caller
|
|
124
|
+
// after an earlier failure must not be dropped.
|
|
125
|
+
if (entry.loading === inflight) {
|
|
126
|
+
entry.loading = undefined
|
|
127
|
+
}
|
|
128
|
+
throw new LazyProviderLoadError(type, cause)
|
|
129
|
+
}
|
|
130
|
+
}
|
|
37
131
|
|
|
38
132
|
/**
|
|
39
133
|
* Central registry for LLM providers.
|
|
@@ -56,6 +150,23 @@ const capabilities = new Map<string, ProviderCapabilities>()
|
|
|
56
150
|
* region: 'us-east-1',
|
|
57
151
|
* })
|
|
58
152
|
* ```
|
|
153
|
+
*
|
|
154
|
+
* Hosts that must not eagerly bundle every provider client register a
|
|
155
|
+
* LOADER instead and construct through the async path:
|
|
156
|
+
*
|
|
157
|
+
* @example
|
|
158
|
+
* ```ts
|
|
159
|
+
* ProviderRegistry.registerLazy(
|
|
160
|
+
* 'anthropic',
|
|
161
|
+
* async () => {
|
|
162
|
+
* const m = await import('@namzu/anthropic')
|
|
163
|
+
* return { create: (c) => new m.AnthropicProvider(c), capabilities: m.ANTHROPIC_CAPABILITIES }
|
|
164
|
+
* },
|
|
165
|
+
* { capabilities: { supportsTools: true, supportsStreaming: true, supportsFunctionCalling: true } },
|
|
166
|
+
* )
|
|
167
|
+
*
|
|
168
|
+
* const { provider } = await ProviderRegistry.createAsync({ type: 'anthropic', apiKey })
|
|
169
|
+
* ```
|
|
59
170
|
*/
|
|
60
171
|
export class ProviderRegistry {
|
|
61
172
|
static register<K extends ProviderType>(
|
|
@@ -64,46 +175,129 @@ export class ProviderRegistry {
|
|
|
64
175
|
caps: ProviderCapabilities,
|
|
65
176
|
options?: RegisterOptions,
|
|
66
177
|
): void {
|
|
67
|
-
if (providers.has(type) && !options?.replace) {
|
|
178
|
+
if ((providers.has(type) || lazyProviders.has(type)) && !options?.replace) {
|
|
68
179
|
throw new DuplicateProviderError(type)
|
|
69
180
|
}
|
|
181
|
+
lazyProviders.delete(type)
|
|
70
182
|
providers.set(type, ctor as LLMProviderConstructor<unknown>)
|
|
71
183
|
capabilities.set(type, caps)
|
|
72
184
|
}
|
|
73
185
|
|
|
186
|
+
/**
|
|
187
|
+
* Register a provider type WITHOUT importing its implementation. The
|
|
188
|
+
* loader is not invoked here; the first `createAsync()` for the type
|
|
189
|
+
* awaits it, validates the resolved `{ create }` module, and caches it.
|
|
190
|
+
* Subsequent creates reuse the cached factory. Only SUCCESS is cached:
|
|
191
|
+
* a rejected load surfaces as `LazyProviderLoadError` and the next
|
|
192
|
+
* `createAsync()` retries the loader. Concurrent first-creates share a
|
|
193
|
+
* single in-flight load.
|
|
194
|
+
*
|
|
195
|
+
* Capability precedence (weakest first):
|
|
196
|
+
* 1. `options.capabilities` — pre-load HINT so `getCapabilities(type)`
|
|
197
|
+
* answers without loading (absent hint ⇒ permissive default, matching
|
|
198
|
+
* `resolveProviderCapabilities`'s treatment of undeclared providers).
|
|
199
|
+
* 2. the loaded module's `capabilities` — replaces the hint on load.
|
|
200
|
+
* 3. the constructed instance's own `LLMProvider.capabilities` — the
|
|
201
|
+
* query runtime negotiates against the INSTANCE
|
|
202
|
+
* (`resolveProviderCapabilities(provider)`), so if it differs from
|
|
203
|
+
* both of the above, the instance wins where it matters.
|
|
204
|
+
*
|
|
205
|
+
* Lazy types are deliberately NOT constructible via the sync
|
|
206
|
+
* `create()`/`createProvider()` (throws `LazyProviderSyncCreateError`),
|
|
207
|
+
* even after the loader has resolved — sync behavior must not depend on
|
|
208
|
+
* whether some earlier call happened to load the module.
|
|
209
|
+
*/
|
|
210
|
+
static registerLazy<K extends ProviderType>(
|
|
211
|
+
type: K,
|
|
212
|
+
loader: LazyProviderLoader<ProviderConfigRegistry[K]>,
|
|
213
|
+
options?: RegisterLazyOptions,
|
|
214
|
+
): void {
|
|
215
|
+
if ((providers.has(type) || lazyProviders.has(type)) && !options?.replace) {
|
|
216
|
+
throw new DuplicateProviderError(type)
|
|
217
|
+
}
|
|
218
|
+
providers.delete(type)
|
|
219
|
+
capabilities.delete(type)
|
|
220
|
+
lazyProviders.set(type, { loader: loader as LazyProviderLoader<unknown> })
|
|
221
|
+
if (options?.capabilities) {
|
|
222
|
+
capabilities.set(type, options.capabilities)
|
|
223
|
+
}
|
|
224
|
+
}
|
|
225
|
+
|
|
74
226
|
static create(config: ProviderFactoryConfig): ProviderFactoryResult {
|
|
75
227
|
const provider = ProviderRegistry.createProvider(config)
|
|
76
228
|
const caps = ProviderRegistry.getCapabilities(config.type)
|
|
77
229
|
return { provider, capabilities: caps }
|
|
78
230
|
}
|
|
79
231
|
|
|
232
|
+
/**
|
|
233
|
+
* Async twin of `create()`. Works for BOTH eager and lazy registrations,
|
|
234
|
+
* so hosts can use one code path; for lazy types it performs the
|
|
235
|
+
* load-on-first-use described on `registerLazy()`.
|
|
236
|
+
*/
|
|
237
|
+
static async createAsync(config: ProviderFactoryConfig): Promise<ProviderFactoryResult> {
|
|
238
|
+
const provider = await ProviderRegistry.createProviderAsync(config)
|
|
239
|
+
// Read capabilities AFTER construction so a lazily-loaded module's
|
|
240
|
+
// authoritative declaration (set during load) is what gets returned.
|
|
241
|
+
const caps = ProviderRegistry.getCapabilities(config.type)
|
|
242
|
+
return { provider, capabilities: caps }
|
|
243
|
+
}
|
|
244
|
+
|
|
80
245
|
static createProvider(config: ProviderFactoryConfig): LLMProvider {
|
|
81
246
|
const Ctor = providers.get(config.type)
|
|
82
247
|
if (!Ctor) {
|
|
248
|
+
if (lazyProviders.has(config.type)) {
|
|
249
|
+
throw new LazyProviderSyncCreateError(config.type)
|
|
250
|
+
}
|
|
83
251
|
throw new UnknownProviderError(config.type)
|
|
84
252
|
}
|
|
85
253
|
return new Ctor(config)
|
|
86
254
|
}
|
|
87
255
|
|
|
256
|
+
static async createProviderAsync(config: ProviderFactoryConfig): Promise<LLMProvider> {
|
|
257
|
+
const Ctor = providers.get(config.type)
|
|
258
|
+
if (Ctor) {
|
|
259
|
+
return new Ctor(config)
|
|
260
|
+
}
|
|
261
|
+
const entry = lazyProviders.get(config.type)
|
|
262
|
+
if (!entry) {
|
|
263
|
+
throw new UnknownProviderError(config.type)
|
|
264
|
+
}
|
|
265
|
+
const mod = await loadLazyModule(config.type, entry)
|
|
266
|
+
return mod.create(config)
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
/**
|
|
270
|
+
* Type-level capabilities. For a lazily-registered type this answers
|
|
271
|
+
* WITHOUT invoking the loader: the registration hint if one was given,
|
|
272
|
+
* otherwise the permissive default (assume everything — consistent with
|
|
273
|
+
* how `resolveProviderCapabilities` treats an undeclared provider). Once
|
|
274
|
+
* loaded, a module-shipped declaration replaces the hint. Note the query
|
|
275
|
+
* runtime negotiates against the constructed INSTANCE's own
|
|
276
|
+
* `capabilities`, which wins over anything stored here.
|
|
277
|
+
*/
|
|
88
278
|
static getCapabilities(type: string): ProviderCapabilities {
|
|
89
279
|
const caps = capabilities.get(type)
|
|
90
|
-
if (
|
|
91
|
-
|
|
280
|
+
if (caps) {
|
|
281
|
+
return caps
|
|
282
|
+
}
|
|
283
|
+
if (lazyProviders.has(type)) {
|
|
284
|
+
return PERMISSIVE_PROVIDER_CAPABILITIES
|
|
92
285
|
}
|
|
93
|
-
|
|
286
|
+
throw new UnknownProviderError(type)
|
|
94
287
|
}
|
|
95
288
|
|
|
96
289
|
static isSupported(type: string): type is ProviderType {
|
|
97
|
-
return providers.has(type)
|
|
290
|
+
return providers.has(type) || lazyProviders.has(type)
|
|
98
291
|
}
|
|
99
292
|
|
|
100
293
|
static unregister(type: ProviderType): boolean {
|
|
101
294
|
capabilities.delete(type)
|
|
102
|
-
|
|
295
|
+
const hadLazy = lazyProviders.delete(type)
|
|
296
|
+
return providers.delete(type) || hadLazy
|
|
103
297
|
}
|
|
104
298
|
|
|
105
299
|
static listTypes(): ProviderType[] {
|
|
106
|
-
return Array.from(providers.keys()) as ProviderType[]
|
|
300
|
+
return Array.from(new Set([...providers.keys(), ...lazyProviders.keys()])) as ProviderType[]
|
|
107
301
|
}
|
|
108
302
|
}
|
|
109
303
|
|
|
@@ -115,4 +309,5 @@ export class ProviderRegistry {
|
|
|
115
309
|
export function __resetProviderRegistryInternal(): void {
|
|
116
310
|
providers.clear()
|
|
117
311
|
capabilities.clear()
|
|
312
|
+
lazyProviders.clear()
|
|
118
313
|
}
|
package/src/public-runtime.ts
CHANGED
|
@@ -70,4 +70,40 @@ export interface RegisterOptions {
|
|
|
70
70
|
replace?: boolean
|
|
71
71
|
}
|
|
72
72
|
|
|
73
|
+
/**
|
|
74
|
+
* What a lazy loader must resolve to: a factory that constructs the
|
|
75
|
+
* provider, plus (optionally) the provider package's authoritative
|
|
76
|
+
* capability declaration resolved at load time.
|
|
77
|
+
*
|
|
78
|
+
* Designed so a host can map a dynamic import in one line:
|
|
79
|
+
*
|
|
80
|
+
* ```ts
|
|
81
|
+
* ProviderRegistry.registerLazy('anthropic', async () => {
|
|
82
|
+
* const m = await import('@namzu/anthropic')
|
|
83
|
+
* return { create: (c) => new m.AnthropicProvider(c), capabilities: m.ANTHROPIC_CAPABILITIES }
|
|
84
|
+
* })
|
|
85
|
+
* ```
|
|
86
|
+
*/
|
|
87
|
+
export interface LazyProviderModule<C = unknown> {
|
|
88
|
+
create: (config: C) => LLMProvider
|
|
89
|
+
/**
|
|
90
|
+
* Authoritative type-level capabilities shipped by the loaded module.
|
|
91
|
+
* When present, replaces any registration-time hint in the registry.
|
|
92
|
+
*/
|
|
93
|
+
capabilities?: ProviderCapabilities
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
export type LazyProviderLoader<C = unknown> = () => Promise<LazyProviderModule<C>>
|
|
97
|
+
|
|
98
|
+
export interface RegisterLazyOptions extends RegisterOptions {
|
|
99
|
+
/**
|
|
100
|
+
* Pre-load capability HINT so `ProviderRegistry.getCapabilities(type)`
|
|
101
|
+
* can answer without triggering the loader. Precedence (weakest first):
|
|
102
|
+
* this hint → the loaded module's `capabilities` → the constructed
|
|
103
|
+
* instance's own `LLMProvider.capabilities` (what the query runtime
|
|
104
|
+
* resolves via `resolveProviderCapabilities` and actually respects).
|
|
105
|
+
*/
|
|
106
|
+
capabilities?: ProviderCapabilities
|
|
107
|
+
}
|
|
108
|
+
|
|
73
109
|
export type LLMProviderConstructor<C = unknown> = new (config: C) => LLMProvider
|