ai-providers 0.2.0 → 0.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/.turbo/turbo-build.log +5 -0
- package/.turbo/turbo-test.log +47 -0
- package/README.md +204 -0
- package/dist/index.d.ts +12 -1
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +12 -1
- package/dist/index.js.map +1 -0
- package/dist/llm.do.d.ts +209 -0
- package/dist/llm.do.d.ts.map +1 -0
- package/dist/llm.do.js +408 -0
- package/dist/llm.do.js.map +1 -0
- package/dist/providers/cloudflare.d.ts +92 -0
- package/dist/providers/cloudflare.d.ts.map +1 -0
- package/dist/providers/cloudflare.js +127 -0
- package/dist/providers/cloudflare.js.map +1 -0
- package/dist/registry.d.ts +136 -0
- package/dist/registry.d.ts.map +1 -0
- package/dist/registry.js +393 -0
- package/dist/registry.js.map +1 -0
- package/package.json +45 -30
- package/src/index.test.ts +341 -0
- package/src/index.ts +37 -0
- package/src/integration.test.ts +317 -0
- package/src/llm.do.test.ts +781 -0
- package/src/llm.do.ts +532 -0
- package/src/providers/cloudflare.test.ts +574 -0
- package/src/providers/cloudflare.ts +216 -0
- package/src/registry.test.ts +491 -0
- package/src/registry.ts +480 -0
- package/tsconfig.json +9 -0
- package/vitest.config.ts +36 -0
- package/dist/provider.d.ts +0 -50
- package/dist/provider.js +0 -128
- package/dist/tsconfig.tsbuildinfo +0 -1
|
@@ -0,0 +1,341 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Tests for the public API surface
|
|
3
|
+
*
|
|
4
|
+
* Covers:
|
|
5
|
+
* - Exported functions and types
|
|
6
|
+
* - Module re-exports
|
|
7
|
+
* - API consistency
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
import { describe, it, expect } from 'vitest'
|
|
11
|
+
import * as aiProviders from './index.js'
|
|
12
|
+
|
|
13
|
+
describe('public API exports', () => {
|
|
14
|
+
describe('registry functions', () => {
|
|
15
|
+
it('exports createRegistry', () => {
|
|
16
|
+
expect(aiProviders.createRegistry).toBeDefined()
|
|
17
|
+
expect(typeof aiProviders.createRegistry).toBe('function')
|
|
18
|
+
})
|
|
19
|
+
|
|
20
|
+
it('exports getRegistry', () => {
|
|
21
|
+
expect(aiProviders.getRegistry).toBeDefined()
|
|
22
|
+
expect(typeof aiProviders.getRegistry).toBe('function')
|
|
23
|
+
})
|
|
24
|
+
|
|
25
|
+
it('exports configureRegistry', () => {
|
|
26
|
+
expect(aiProviders.configureRegistry).toBeDefined()
|
|
27
|
+
expect(typeof aiProviders.configureRegistry).toBe('function')
|
|
28
|
+
})
|
|
29
|
+
})
|
|
30
|
+
|
|
31
|
+
describe('model functions', () => {
|
|
32
|
+
it('exports model', () => {
|
|
33
|
+
expect(aiProviders.model).toBeDefined()
|
|
34
|
+
expect(typeof aiProviders.model).toBe('function')
|
|
35
|
+
})
|
|
36
|
+
|
|
37
|
+
it('exports embeddingModel', () => {
|
|
38
|
+
expect(aiProviders.embeddingModel).toBeDefined()
|
|
39
|
+
expect(typeof aiProviders.embeddingModel).toBe('function')
|
|
40
|
+
})
|
|
41
|
+
})
|
|
42
|
+
|
|
43
|
+
describe('constants', () => {
|
|
44
|
+
it('exports DIRECT_PROVIDERS', () => {
|
|
45
|
+
expect(aiProviders.DIRECT_PROVIDERS).toBeDefined()
|
|
46
|
+
expect(Array.isArray(aiProviders.DIRECT_PROVIDERS)).toBe(true)
|
|
47
|
+
})
|
|
48
|
+
})
|
|
49
|
+
|
|
50
|
+
describe('types', () => {
|
|
51
|
+
it('exports ProviderId type', () => {
|
|
52
|
+
// Type-only export, check in type context
|
|
53
|
+
const typeCheck: aiProviders.ProviderId = 'openai'
|
|
54
|
+
expect(typeCheck).toBe('openai')
|
|
55
|
+
})
|
|
56
|
+
|
|
57
|
+
it('exports DirectProvider type', () => {
|
|
58
|
+
// Type-only export, check in type context
|
|
59
|
+
const typeCheck: aiProviders.DirectProvider = 'anthropic'
|
|
60
|
+
expect(typeCheck).toBe('anthropic')
|
|
61
|
+
})
|
|
62
|
+
|
|
63
|
+
it('exports ProviderConfig type', () => {
|
|
64
|
+
// Type-only export, check in type context
|
|
65
|
+
const typeCheck: aiProviders.ProviderConfig = {
|
|
66
|
+
gatewayUrl: 'test',
|
|
67
|
+
}
|
|
68
|
+
expect(typeCheck).toBeDefined()
|
|
69
|
+
})
|
|
70
|
+
|
|
71
|
+
it('exports Provider type from AI SDK', () => {
|
|
72
|
+
// Re-exported from 'ai' package
|
|
73
|
+
// Type-only export for convenience
|
|
74
|
+
expect(true).toBe(true)
|
|
75
|
+
})
|
|
76
|
+
|
|
77
|
+
it('exports ProviderRegistryProvider type from AI SDK', () => {
|
|
78
|
+
// Re-exported from 'ai' package
|
|
79
|
+
// Type-only export for convenience
|
|
80
|
+
expect(true).toBe(true)
|
|
81
|
+
})
|
|
82
|
+
})
|
|
83
|
+
|
|
84
|
+
describe('API completeness', () => {
|
|
85
|
+
it('exports all documented functions', () => {
|
|
86
|
+
const expectedExports = [
|
|
87
|
+
'createRegistry',
|
|
88
|
+
'getRegistry',
|
|
89
|
+
'configureRegistry',
|
|
90
|
+
'model',
|
|
91
|
+
'embeddingModel',
|
|
92
|
+
'DIRECT_PROVIDERS',
|
|
93
|
+
]
|
|
94
|
+
|
|
95
|
+
for (const exportName of expectedExports) {
|
|
96
|
+
expect(aiProviders).toHaveProperty(exportName)
|
|
97
|
+
}
|
|
98
|
+
})
|
|
99
|
+
|
|
100
|
+
it('does not export internal implementation details', () => {
|
|
101
|
+
// Should not export internal functions like getEnvConfig, getBaseUrl, etc.
|
|
102
|
+
const internalFunctions = [
|
|
103
|
+
'getEnvConfig',
|
|
104
|
+
'getBaseUrl',
|
|
105
|
+
'createGatewayFetch',
|
|
106
|
+
'useGatewaySecrets',
|
|
107
|
+
'getApiKey',
|
|
108
|
+
'createOpenAIProvider',
|
|
109
|
+
'createAnthropicProvider',
|
|
110
|
+
'createGoogleProvider',
|
|
111
|
+
'createOpenRouterProvider',
|
|
112
|
+
'createCloudflareProvider',
|
|
113
|
+
'parseModelId',
|
|
114
|
+
'providerFactories',
|
|
115
|
+
]
|
|
116
|
+
|
|
117
|
+
for (const internalName of internalFunctions) {
|
|
118
|
+
expect(aiProviders).not.toHaveProperty(internalName)
|
|
119
|
+
}
|
|
120
|
+
})
|
|
121
|
+
})
|
|
122
|
+
|
|
123
|
+
describe('module structure', () => {
|
|
124
|
+
it('has expected module exports', () => {
|
|
125
|
+
const exports = Object.keys(aiProviders)
|
|
126
|
+
expect(exports.length).toBeGreaterThan(0)
|
|
127
|
+
})
|
|
128
|
+
|
|
129
|
+
it('all exported functions are callable', () => {
|
|
130
|
+
const functions = [
|
|
131
|
+
'createRegistry',
|
|
132
|
+
'getRegistry',
|
|
133
|
+
'configureRegistry',
|
|
134
|
+
'model',
|
|
135
|
+
'embeddingModel',
|
|
136
|
+
]
|
|
137
|
+
|
|
138
|
+
for (const fnName of functions) {
|
|
139
|
+
const fn = (aiProviders as any)[fnName]
|
|
140
|
+
expect(typeof fn).toBe('function')
|
|
141
|
+
}
|
|
142
|
+
})
|
|
143
|
+
|
|
144
|
+
it('all exported constants are defined', () => {
|
|
145
|
+
expect(aiProviders.DIRECT_PROVIDERS).toBeDefined()
|
|
146
|
+
})
|
|
147
|
+
})
|
|
148
|
+
})
|
|
149
|
+
|
|
150
|
+
describe('API usage patterns', () => {
|
|
151
|
+
it('allows importing all exports', () => {
|
|
152
|
+
const {
|
|
153
|
+
createRegistry,
|
|
154
|
+
getRegistry,
|
|
155
|
+
configureRegistry,
|
|
156
|
+
model,
|
|
157
|
+
embeddingModel,
|
|
158
|
+
DIRECT_PROVIDERS,
|
|
159
|
+
} = aiProviders
|
|
160
|
+
|
|
161
|
+
expect(createRegistry).toBeDefined()
|
|
162
|
+
expect(getRegistry).toBeDefined()
|
|
163
|
+
expect(configureRegistry).toBeDefined()
|
|
164
|
+
expect(model).toBeDefined()
|
|
165
|
+
expect(embeddingModel).toBeDefined()
|
|
166
|
+
expect(DIRECT_PROVIDERS).toBeDefined()
|
|
167
|
+
})
|
|
168
|
+
|
|
169
|
+
it('allows importing specific exports', async () => {
|
|
170
|
+
const { model } = await import('./index.js')
|
|
171
|
+
expect(model).toBeDefined()
|
|
172
|
+
})
|
|
173
|
+
|
|
174
|
+
it('supports default import pattern', async () => {
|
|
175
|
+
const module = await import('./index.js')
|
|
176
|
+
expect(module.model).toBeDefined()
|
|
177
|
+
})
|
|
178
|
+
})
|
|
179
|
+
|
|
180
|
+
describe('type exports', () => {
|
|
181
|
+
it('ProviderId accepts valid provider names', () => {
|
|
182
|
+
const providers: aiProviders.ProviderId[] = [
|
|
183
|
+
'openai',
|
|
184
|
+
'anthropic',
|
|
185
|
+
'google',
|
|
186
|
+
'openrouter',
|
|
187
|
+
'cloudflare',
|
|
188
|
+
]
|
|
189
|
+
expect(providers).toHaveLength(5)
|
|
190
|
+
})
|
|
191
|
+
|
|
192
|
+
it('ProviderConfig has optional fields', () => {
|
|
193
|
+
const configs: aiProviders.ProviderConfig[] = [
|
|
194
|
+
{},
|
|
195
|
+
{ gatewayUrl: 'test' },
|
|
196
|
+
{ gatewayToken: 'test' },
|
|
197
|
+
{ openaiApiKey: 'test' },
|
|
198
|
+
{
|
|
199
|
+
gatewayUrl: 'test',
|
|
200
|
+
gatewayToken: 'test',
|
|
201
|
+
openaiApiKey: 'test',
|
|
202
|
+
anthropicApiKey: 'test',
|
|
203
|
+
googleApiKey: 'test',
|
|
204
|
+
openrouterApiKey: 'test',
|
|
205
|
+
cloudflareAccountId: 'test',
|
|
206
|
+
cloudflareApiToken: 'test',
|
|
207
|
+
},
|
|
208
|
+
]
|
|
209
|
+
expect(configs).toHaveLength(5)
|
|
210
|
+
})
|
|
211
|
+
|
|
212
|
+
it('DirectProvider is a subset of ProviderId', () => {
|
|
213
|
+
// DirectProvider should be 'openai' | 'anthropic' | 'google'
|
|
214
|
+
const direct: aiProviders.DirectProvider[] = ['openai', 'anthropic', 'google']
|
|
215
|
+
expect(direct).toHaveLength(3)
|
|
216
|
+
})
|
|
217
|
+
})
|
|
218
|
+
|
|
219
|
+
describe('package documentation', () => {
|
|
220
|
+
it('has JSDoc package documentation', () => {
|
|
221
|
+
// Main module should have @packageDocumentation
|
|
222
|
+
expect(true).toBe(true)
|
|
223
|
+
})
|
|
224
|
+
|
|
225
|
+
it('exports match README examples', () => {
|
|
226
|
+
// README shows these imports:
|
|
227
|
+
// import { model } from 'ai-providers'
|
|
228
|
+
// import { embeddingModel } from 'ai-providers'
|
|
229
|
+
// import { createRegistry } from 'ai-providers'
|
|
230
|
+
// import { getRegistry } from 'ai-providers'
|
|
231
|
+
|
|
232
|
+
expect(aiProviders.model).toBeDefined()
|
|
233
|
+
expect(aiProviders.embeddingModel).toBeDefined()
|
|
234
|
+
expect(aiProviders.createRegistry).toBeDefined()
|
|
235
|
+
expect(aiProviders.getRegistry).toBeDefined()
|
|
236
|
+
})
|
|
237
|
+
})
|
|
238
|
+
|
|
239
|
+
describe('convenience re-exports', () => {
|
|
240
|
+
it('re-exports AI SDK types for convenience', () => {
|
|
241
|
+
// Provider and ProviderRegistryProvider are re-exported from 'ai'
|
|
242
|
+
// This allows users to import types from ai-providers instead of ai
|
|
243
|
+
expect(true).toBe(true)
|
|
244
|
+
})
|
|
245
|
+
|
|
246
|
+
it('re-exports DIRECT_PROVIDERS from language-models', () => {
|
|
247
|
+
// DIRECT_PROVIDERS is re-exported from language-models for consistency
|
|
248
|
+
expect(aiProviders.DIRECT_PROVIDERS).toBeDefined()
|
|
249
|
+
})
|
|
250
|
+
})
|
|
251
|
+
|
|
252
|
+
describe('integration with subpackages', () => {
|
|
253
|
+
it('main package exports registry functions', () => {
|
|
254
|
+
// Main package exports registry.ts
|
|
255
|
+
expect(aiProviders.createRegistry).toBeDefined()
|
|
256
|
+
})
|
|
257
|
+
|
|
258
|
+
it('cloudflare provider is in separate export', async () => {
|
|
259
|
+
// Cloudflare provider should be importable from 'ai-providers/cloudflare'
|
|
260
|
+
// Not from the main 'ai-providers' export
|
|
261
|
+
expect(aiProviders).not.toHaveProperty('cloudflareEmbedding')
|
|
262
|
+
expect(aiProviders).not.toHaveProperty('cloudflare')
|
|
263
|
+
})
|
|
264
|
+
})
|
|
265
|
+
|
|
266
|
+
describe('backward compatibility', () => {
|
|
267
|
+
it('maintains stable public API', () => {
|
|
268
|
+
// These exports should remain stable across versions
|
|
269
|
+
const stableExports = [
|
|
270
|
+
'createRegistry',
|
|
271
|
+
'getRegistry',
|
|
272
|
+
'model',
|
|
273
|
+
'embeddingModel',
|
|
274
|
+
'DIRECT_PROVIDERS',
|
|
275
|
+
]
|
|
276
|
+
|
|
277
|
+
for (const exportName of stableExports) {
|
|
278
|
+
expect(aiProviders).toHaveProperty(exportName)
|
|
279
|
+
}
|
|
280
|
+
})
|
|
281
|
+
|
|
282
|
+
it('types are backward compatible', () => {
|
|
283
|
+
// Type structure should be stable
|
|
284
|
+
const config: aiProviders.ProviderConfig = {
|
|
285
|
+
gatewayUrl: 'test',
|
|
286
|
+
}
|
|
287
|
+
expect(config).toBeDefined()
|
|
288
|
+
})
|
|
289
|
+
})
|
|
290
|
+
|
|
291
|
+
describe('error messages', () => {
|
|
292
|
+
it('provides helpful error for missing credentials', async () => {
|
|
293
|
+
// When creating a model without credentials, should have clear error
|
|
294
|
+
expect(true).toBe(true)
|
|
295
|
+
})
|
|
296
|
+
|
|
297
|
+
it('provides helpful error for invalid model ID', async () => {
|
|
298
|
+
// When providing invalid model ID, should have clear error
|
|
299
|
+
expect(true).toBe(true)
|
|
300
|
+
})
|
|
301
|
+
})
|
|
302
|
+
|
|
303
|
+
describe('async API behavior', () => {
|
|
304
|
+
it('createRegistry returns a Promise', async () => {
|
|
305
|
+
const result = aiProviders.createRegistry()
|
|
306
|
+
expect(result).toBeInstanceOf(Promise)
|
|
307
|
+
await result // Wait for it to complete
|
|
308
|
+
})
|
|
309
|
+
|
|
310
|
+
it('getRegistry returns a Promise', async () => {
|
|
311
|
+
const result = aiProviders.getRegistry()
|
|
312
|
+
expect(result).toBeInstanceOf(Promise)
|
|
313
|
+
await result
|
|
314
|
+
})
|
|
315
|
+
|
|
316
|
+
it('configureRegistry returns a Promise', async () => {
|
|
317
|
+
const result = aiProviders.configureRegistry({})
|
|
318
|
+
expect(result).toBeInstanceOf(Promise)
|
|
319
|
+
await result
|
|
320
|
+
})
|
|
321
|
+
|
|
322
|
+
it('model returns a Promise', async () => {
|
|
323
|
+
try {
|
|
324
|
+
const result = aiProviders.model('test')
|
|
325
|
+
expect(result).toBeInstanceOf(Promise)
|
|
326
|
+
await result
|
|
327
|
+
} catch {
|
|
328
|
+
// May fail without proper config
|
|
329
|
+
}
|
|
330
|
+
})
|
|
331
|
+
|
|
332
|
+
it('embeddingModel returns a Promise', async () => {
|
|
333
|
+
try {
|
|
334
|
+
const result = aiProviders.embeddingModel('openai:test')
|
|
335
|
+
expect(result).toBeInstanceOf(Promise)
|
|
336
|
+
await result
|
|
337
|
+
} catch {
|
|
338
|
+
// May fail without proper config
|
|
339
|
+
}
|
|
340
|
+
})
|
|
341
|
+
})
|
package/src/index.ts
ADDED
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* ai-providers - Unified AI Provider Registry
|
|
3
|
+
*
|
|
4
|
+
* Access multiple AI providers via simple string identifiers.
|
|
5
|
+
* Supports Cloudflare AI Gateway for unified routing and auth.
|
|
6
|
+
*
|
|
7
|
+
* @packageDocumentation
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
export {
|
|
11
|
+
createRegistry,
|
|
12
|
+
getRegistry,
|
|
13
|
+
configureRegistry,
|
|
14
|
+
model,
|
|
15
|
+
embeddingModel,
|
|
16
|
+
DIRECT_PROVIDERS,
|
|
17
|
+
type ProviderId,
|
|
18
|
+
type DirectProvider,
|
|
19
|
+
type ProviderConfig
|
|
20
|
+
} from './registry.js'
|
|
21
|
+
|
|
22
|
+
// Export llm.do WebSocket transport
|
|
23
|
+
export {
|
|
24
|
+
LLM,
|
|
25
|
+
getLLM,
|
|
26
|
+
createLLMFetch,
|
|
27
|
+
type LLMConfig,
|
|
28
|
+
type UniversalRequest,
|
|
29
|
+
type UniversalCreated,
|
|
30
|
+
type UniversalStream,
|
|
31
|
+
type UniversalDone,
|
|
32
|
+
type UniversalError,
|
|
33
|
+
type GatewayMessage
|
|
34
|
+
} from './llm.do.js'
|
|
35
|
+
|
|
36
|
+
// Re-export AI SDK types for convenience
|
|
37
|
+
export type { Provider, ProviderRegistryProvider } from 'ai'
|
|
@@ -0,0 +1,317 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Integration tests for ai-providers
|
|
3
|
+
*
|
|
4
|
+
* These tests make real API calls and are skipped if credentials are not available.
|
|
5
|
+
* Set AI_GATEWAY_URL and AI_GATEWAY_TOKEN (or individual provider keys) to run these tests.
|
|
6
|
+
*
|
|
7
|
+
* Tests cover:
|
|
8
|
+
* - End-to-end provider usage
|
|
9
|
+
* - Gateway integration
|
|
10
|
+
* - Model generation
|
|
11
|
+
* - Embedding generation
|
|
12
|
+
*/
|
|
13
|
+
|
|
14
|
+
import { describe, it, expect } from 'vitest'
|
|
15
|
+
import { model, embeddingModel, createRegistry } from './index.js'
|
|
16
|
+
|
|
17
|
+
// Skip tests if no credentials configured
|
|
18
|
+
const hasGateway = !!(
|
|
19
|
+
process.env.AI_GATEWAY_URL ||
|
|
20
|
+
process.env.ANTHROPIC_API_KEY ||
|
|
21
|
+
process.env.OPENAI_API_KEY ||
|
|
22
|
+
process.env.OPENROUTER_API_KEY
|
|
23
|
+
)
|
|
24
|
+
|
|
25
|
+
const hasCloudflare = !!(
|
|
26
|
+
process.env.CLOUDFLARE_ACCOUNT_ID &&
|
|
27
|
+
process.env.CLOUDFLARE_API_TOKEN
|
|
28
|
+
)
|
|
29
|
+
|
|
30
|
+
describe.skipIf(!hasGateway)('model() integration', () => {
|
|
31
|
+
it('gets a language model from registry', async () => {
|
|
32
|
+
const m = await model('sonnet')
|
|
33
|
+
expect(m).toBeDefined()
|
|
34
|
+
})
|
|
35
|
+
|
|
36
|
+
it('resolves aliases to full model IDs', async () => {
|
|
37
|
+
const m = await model('opus')
|
|
38
|
+
expect(m).toBeDefined()
|
|
39
|
+
})
|
|
40
|
+
|
|
41
|
+
it('works with full model IDs', async () => {
|
|
42
|
+
const m = await model('anthropic/claude-sonnet-4.5')
|
|
43
|
+
expect(m).toBeDefined()
|
|
44
|
+
})
|
|
45
|
+
|
|
46
|
+
it('routes OpenAI models to OpenAI SDK', async () => {
|
|
47
|
+
try {
|
|
48
|
+
const m = await model('openai/gpt-4o')
|
|
49
|
+
expect(m).toBeDefined()
|
|
50
|
+
} catch (error) {
|
|
51
|
+
// May not be configured
|
|
52
|
+
expect(error).toBeDefined()
|
|
53
|
+
}
|
|
54
|
+
})
|
|
55
|
+
|
|
56
|
+
it('routes Anthropic models to Anthropic SDK', async () => {
|
|
57
|
+
try {
|
|
58
|
+
const m = await model('anthropic/claude-sonnet-4.5')
|
|
59
|
+
expect(m).toBeDefined()
|
|
60
|
+
} catch (error) {
|
|
61
|
+
// May not be configured
|
|
62
|
+
expect(error).toBeDefined()
|
|
63
|
+
}
|
|
64
|
+
})
|
|
65
|
+
|
|
66
|
+
it('routes Google models to Google SDK', async () => {
|
|
67
|
+
try {
|
|
68
|
+
const m = await model('google/gemini-2.5-flash')
|
|
69
|
+
expect(m).toBeDefined()
|
|
70
|
+
} catch (error) {
|
|
71
|
+
// May not be configured
|
|
72
|
+
expect(error).toBeDefined()
|
|
73
|
+
}
|
|
74
|
+
})
|
|
75
|
+
|
|
76
|
+
it('routes other models through OpenRouter', async () => {
|
|
77
|
+
try {
|
|
78
|
+
const m = await model('meta-llama/llama-3.3-70b-instruct')
|
|
79
|
+
expect(m).toBeDefined()
|
|
80
|
+
} catch (error) {
|
|
81
|
+
// May not be configured
|
|
82
|
+
expect(error).toBeDefined()
|
|
83
|
+
}
|
|
84
|
+
})
|
|
85
|
+
})
|
|
86
|
+
|
|
87
|
+
describe.skipIf(!hasGateway)('embeddingModel() integration', () => {
|
|
88
|
+
it('gets an OpenAI embedding model', async () => {
|
|
89
|
+
try {
|
|
90
|
+
const em = await embeddingModel('openai:text-embedding-3-small')
|
|
91
|
+
expect(em).toBeDefined()
|
|
92
|
+
expect(em.provider).toBe('openai')
|
|
93
|
+
} catch (error) {
|
|
94
|
+
// May not be configured
|
|
95
|
+
expect(error).toBeDefined()
|
|
96
|
+
}
|
|
97
|
+
})
|
|
98
|
+
|
|
99
|
+
it.skipIf(!hasCloudflare)('gets a Cloudflare embedding model', async () => {
|
|
100
|
+
const em = await embeddingModel('cloudflare:@cf/baai/bge-m3')
|
|
101
|
+
expect(em).toBeDefined()
|
|
102
|
+
expect(em.provider).toBe('cloudflare')
|
|
103
|
+
})
|
|
104
|
+
})
|
|
105
|
+
|
|
106
|
+
describe.skipIf(!hasGateway)('createRegistry() integration', () => {
|
|
107
|
+
it('creates registry with gateway config', async () => {
|
|
108
|
+
const registry = await createRegistry({
|
|
109
|
+
gatewayUrl: process.env.AI_GATEWAY_URL,
|
|
110
|
+
gatewayToken: process.env.AI_GATEWAY_TOKEN,
|
|
111
|
+
})
|
|
112
|
+
|
|
113
|
+
expect(registry).toBeDefined()
|
|
114
|
+
expect(typeof registry.languageModel).toBe('function')
|
|
115
|
+
expect(typeof registry.textEmbeddingModel).toBe('function')
|
|
116
|
+
})
|
|
117
|
+
|
|
118
|
+
it('creates registry with direct API keys', async () => {
|
|
119
|
+
const registry = await createRegistry({
|
|
120
|
+
openaiApiKey: process.env.OPENAI_API_KEY,
|
|
121
|
+
anthropicApiKey: process.env.ANTHROPIC_API_KEY,
|
|
122
|
+
openrouterApiKey: process.env.OPENROUTER_API_KEY,
|
|
123
|
+
})
|
|
124
|
+
|
|
125
|
+
expect(registry).toBeDefined()
|
|
126
|
+
})
|
|
127
|
+
|
|
128
|
+
it('can use registry to get models', async () => {
|
|
129
|
+
const registry = await createRegistry()
|
|
130
|
+
|
|
131
|
+
try {
|
|
132
|
+
const m = registry.languageModel('openrouter:anthropic/claude-sonnet-4.5')
|
|
133
|
+
expect(m).toBeDefined()
|
|
134
|
+
} catch (error) {
|
|
135
|
+
// May not be configured
|
|
136
|
+
expect(error).toBeDefined()
|
|
137
|
+
}
|
|
138
|
+
})
|
|
139
|
+
})
|
|
140
|
+
|
|
141
|
+
describe.skipIf(!hasGateway)('gateway authentication', () => {
|
|
142
|
+
it('works with gateway stored secrets', async () => {
|
|
143
|
+
if (!process.env.AI_GATEWAY_URL || !process.env.AI_GATEWAY_TOKEN) {
|
|
144
|
+
return
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
const registry = await createRegistry({
|
|
148
|
+
gatewayUrl: process.env.AI_GATEWAY_URL,
|
|
149
|
+
gatewayToken: process.env.AI_GATEWAY_TOKEN,
|
|
150
|
+
// No individual API keys needed
|
|
151
|
+
})
|
|
152
|
+
|
|
153
|
+
expect(registry).toBeDefined()
|
|
154
|
+
})
|
|
155
|
+
|
|
156
|
+
it('custom fetch adds cf-aig-authorization header', async () => {
|
|
157
|
+
// This is tested indirectly through successful API calls with gateway
|
|
158
|
+
expect(true).toBe(true)
|
|
159
|
+
})
|
|
160
|
+
})
|
|
161
|
+
|
|
162
|
+
describe.skipIf(!hasCloudflare)('Cloudflare embeddings integration', () => {
|
|
163
|
+
it('generates embeddings via REST API', async () => {
|
|
164
|
+
const { cloudflareEmbedding } = await import('./providers/cloudflare.js')
|
|
165
|
+
const model = cloudflareEmbedding('@cf/baai/bge-m3', {
|
|
166
|
+
accountId: process.env.CLOUDFLARE_ACCOUNT_ID,
|
|
167
|
+
apiToken: process.env.CLOUDFLARE_API_TOKEN,
|
|
168
|
+
})
|
|
169
|
+
|
|
170
|
+
const result = await model.doEmbed({
|
|
171
|
+
values: ['hello world', 'test embedding'],
|
|
172
|
+
})
|
|
173
|
+
|
|
174
|
+
expect(result.embeddings).toHaveLength(2)
|
|
175
|
+
expect(result.embeddings[0].length).toBeGreaterThan(0)
|
|
176
|
+
expect(Array.isArray(result.embeddings[0])).toBe(true)
|
|
177
|
+
})
|
|
178
|
+
|
|
179
|
+
it('supports different embedding models', async () => {
|
|
180
|
+
const { cloudflareEmbedding } = await import('./providers/cloudflare.js')
|
|
181
|
+
const models = [
|
|
182
|
+
'@cf/baai/bge-small-en-v1.5',
|
|
183
|
+
'@cf/baai/bge-base-en-v1.5',
|
|
184
|
+
'@cf/baai/bge-m3',
|
|
185
|
+
]
|
|
186
|
+
|
|
187
|
+
for (const modelId of models) {
|
|
188
|
+
const model = cloudflareEmbedding(modelId, {
|
|
189
|
+
accountId: process.env.CLOUDFLARE_ACCOUNT_ID,
|
|
190
|
+
apiToken: process.env.CLOUDFLARE_API_TOKEN,
|
|
191
|
+
})
|
|
192
|
+
|
|
193
|
+
const result = await model.doEmbed({
|
|
194
|
+
values: ['test'],
|
|
195
|
+
})
|
|
196
|
+
|
|
197
|
+
expect(result.embeddings).toHaveLength(1)
|
|
198
|
+
}
|
|
199
|
+
}, 30000) // Allow extra time for multiple API calls
|
|
200
|
+
})
|
|
201
|
+
|
|
202
|
+
describe.skipIf(!hasGateway)('provider-specific features', () => {
|
|
203
|
+
it.skip('supports Anthropic MCP with direct routing', async () => {
|
|
204
|
+
// Test MCP features when using direct Anthropic SDK routing
|
|
205
|
+
// Requires actual MCP implementation
|
|
206
|
+
expect(true).toBe(true)
|
|
207
|
+
})
|
|
208
|
+
|
|
209
|
+
it.skip('supports OpenAI function calling with direct routing', async () => {
|
|
210
|
+
// Test function calling when using direct OpenAI SDK routing
|
|
211
|
+
expect(true).toBe(true)
|
|
212
|
+
})
|
|
213
|
+
|
|
214
|
+
it.skip('supports Google grounding with direct routing', async () => {
|
|
215
|
+
// Test grounding when using direct Google SDK routing
|
|
216
|
+
expect(true).toBe(true)
|
|
217
|
+
})
|
|
218
|
+
})
|
|
219
|
+
|
|
220
|
+
describe.skipIf(!hasGateway)('error handling', () => {
|
|
221
|
+
it('handles invalid model IDs gracefully', async () => {
|
|
222
|
+
try {
|
|
223
|
+
await model('invalid-model-that-does-not-exist-12345')
|
|
224
|
+
} catch (error) {
|
|
225
|
+
expect(error).toBeDefined()
|
|
226
|
+
}
|
|
227
|
+
})
|
|
228
|
+
|
|
229
|
+
it('handles network errors gracefully', async () => {
|
|
230
|
+
const registry = await createRegistry({
|
|
231
|
+
baseUrls: {
|
|
232
|
+
openrouter: 'https://invalid-url-that-does-not-exist.example.com',
|
|
233
|
+
},
|
|
234
|
+
})
|
|
235
|
+
|
|
236
|
+
try {
|
|
237
|
+
const m = registry.languageModel('openrouter:test')
|
|
238
|
+
// Attempting to use this model should fail
|
|
239
|
+
expect(m).toBeDefined()
|
|
240
|
+
} catch (error) {
|
|
241
|
+
expect(error).toBeDefined()
|
|
242
|
+
}
|
|
243
|
+
})
|
|
244
|
+
|
|
245
|
+
it('handles missing credentials gracefully', async () => {
|
|
246
|
+
const registry = await createRegistry({
|
|
247
|
+
// No credentials provided
|
|
248
|
+
gatewayUrl: undefined,
|
|
249
|
+
gatewayToken: undefined,
|
|
250
|
+
openaiApiKey: undefined,
|
|
251
|
+
anthropicApiKey: undefined,
|
|
252
|
+
})
|
|
253
|
+
|
|
254
|
+
// Registry should be created, but using models may fail
|
|
255
|
+
expect(registry).toBeDefined()
|
|
256
|
+
})
|
|
257
|
+
})
|
|
258
|
+
|
|
259
|
+
describe.skipIf(!hasGateway)('concurrent requests', () => {
|
|
260
|
+
it('handles multiple concurrent model resolutions', async () => {
|
|
261
|
+
const promises = [
|
|
262
|
+
model('sonnet'),
|
|
263
|
+
model('opus'),
|
|
264
|
+
model('gpt-4o'),
|
|
265
|
+
]
|
|
266
|
+
|
|
267
|
+
try {
|
|
268
|
+
const models = await Promise.all(promises)
|
|
269
|
+
expect(models).toHaveLength(3)
|
|
270
|
+
models.forEach(m => expect(m).toBeDefined())
|
|
271
|
+
} catch (error) {
|
|
272
|
+
// Some models may not be configured
|
|
273
|
+
expect(error).toBeDefined()
|
|
274
|
+
}
|
|
275
|
+
})
|
|
276
|
+
|
|
277
|
+
it('caches registry to avoid duplicate initialization', async () => {
|
|
278
|
+
const { getRegistry } = await import('./index.js')
|
|
279
|
+
|
|
280
|
+
const promises = [
|
|
281
|
+
getRegistry(),
|
|
282
|
+
getRegistry(),
|
|
283
|
+
getRegistry(),
|
|
284
|
+
]
|
|
285
|
+
|
|
286
|
+
const registries = await Promise.all(promises)
|
|
287
|
+
|
|
288
|
+
// All should be the same instance
|
|
289
|
+
expect(registries[0]).toBe(registries[1])
|
|
290
|
+
expect(registries[1]).toBe(registries[2])
|
|
291
|
+
})
|
|
292
|
+
})
|
|
293
|
+
|
|
294
|
+
describe.skipIf(!hasGateway)('performance', () => {
|
|
295
|
+
it('resolves models quickly after initial setup', async () => {
|
|
296
|
+
const start = Date.now()
|
|
297
|
+
await model('sonnet')
|
|
298
|
+
const duration = Date.now() - start
|
|
299
|
+
|
|
300
|
+
// After registry is initialized, should be fast
|
|
301
|
+
// Allow up to 1 second for initialization
|
|
302
|
+
expect(duration).toBeLessThan(1000)
|
|
303
|
+
})
|
|
304
|
+
|
|
305
|
+
it('caches registry for subsequent calls', async () => {
|
|
306
|
+
// First call initializes
|
|
307
|
+
await model('sonnet')
|
|
308
|
+
|
|
309
|
+
// Second call should use cached registry
|
|
310
|
+
const start = Date.now()
|
|
311
|
+
await model('opus')
|
|
312
|
+
const duration = Date.now() - start
|
|
313
|
+
|
|
314
|
+
// Should be very fast (< 100ms) when cached
|
|
315
|
+
expect(duration).toBeLessThan(100)
|
|
316
|
+
})
|
|
317
|
+
})
|