openllmprovider 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +192 -0
- package/dist/auth/index.cjs +6 -0
- package/dist/auth/index.d.cts +3 -0
- package/dist/auth/index.d.mts +3 -0
- package/dist/auth/index.mjs +3 -0
- package/dist/auto-C2hXJY13.d.cts +33 -0
- package/dist/auto-C2hXJY13.d.cts.map +1 -0
- package/dist/auto-CBqNYBXs.mjs +48 -0
- package/dist/auto-CBqNYBXs.mjs.map +1 -0
- package/dist/auto-CInerwvs.d.mts +33 -0
- package/dist/auto-CInerwvs.d.mts.map +1 -0
- package/dist/auto-D77wgMqO.cjs +59 -0
- package/dist/auto-D77wgMqO.cjs.map +1 -0
- package/dist/file-DB-rxfzi.mjs +77 -0
- package/dist/file-DB-rxfzi.mjs.map +1 -0
- package/dist/file-DZ7FGcSW.cjs +73 -0
- package/dist/file-DZ7FGcSW.cjs.map +1 -0
- package/dist/index.cjs +1909 -0
- package/dist/index.cjs.map +1 -0
- package/dist/index.d.cts +1239 -0
- package/dist/index.d.cts.map +1 -0
- package/dist/index.d.mts +1241 -0
- package/dist/index.d.mts.map +1 -0
- package/dist/index.mjs +1891 -0
- package/dist/index.mjs.map +1 -0
- package/dist/logger-BsHpI_fH.mjs +11 -0
- package/dist/logger-BsHpI_fH.mjs.map +1 -0
- package/dist/logger-jRimlMFR.cjs +69 -0
- package/dist/logger-jRimlMFR.cjs.map +1 -0
- package/dist/plugin/index.cjs +29 -0
- package/dist/plugin/index.cjs.map +1 -0
- package/dist/plugin/index.d.cts +10 -0
- package/dist/plugin/index.d.cts.map +1 -0
- package/dist/plugin/index.d.mts +10 -0
- package/dist/plugin/index.d.mts.map +1 -0
- package/dist/plugin/index.mjs +25 -0
- package/dist/plugin/index.mjs.map +1 -0
- package/dist/plugin-BkeUu5LW.d.mts +46 -0
- package/dist/plugin-BkeUu5LW.d.mts.map +1 -0
- package/dist/plugin-wK7RmJhZ.d.cts +46 -0
- package/dist/plugin-wK7RmJhZ.d.cts.map +1 -0
- package/dist/resolver-BA7LWSJO.mjs +645 -0
- package/dist/resolver-BA7LWSJO.mjs.map +1 -0
- package/dist/resolver-BMTvzTt9.cjs +662 -0
- package/dist/resolver-BMTvzTt9.cjs.map +1 -0
- package/dist/resolver-MgJryMWG.d.cts +75 -0
- package/dist/resolver-MgJryMWG.d.cts.map +1 -0
- package/dist/resolver-_gfXzr_S.d.mts +76 -0
- package/dist/resolver-_gfXzr_S.d.mts.map +1 -0
- package/dist/storage/index.cjs +7 -0
- package/dist/storage/index.d.cts +12 -0
- package/dist/storage/index.d.cts.map +1 -0
- package/dist/storage/index.d.mts +12 -0
- package/dist/storage/index.d.mts.map +1 -0
- package/dist/storage/index.mjs +4 -0
- package/package.json +137 -0
- package/src/auth/.gitkeep +0 -0
- package/src/auth/index.ts +10 -0
- package/src/auth/resolver.ts +46 -0
- package/src/auth/scanners.ts +462 -0
- package/src/auth/store.ts +357 -0
- package/src/catalog/.gitkeep +0 -0
- package/src/catalog/catalog.ts +302 -0
- package/src/catalog/index.ts +17 -0
- package/src/catalog/mapper.ts +129 -0
- package/src/catalog/merger.ts +99 -0
- package/src/index.ts +37 -0
- package/src/logger.ts +7 -0
- package/src/plugin/.gitkeep +0 -0
- package/src/plugin/anthropic.test.ts +505 -0
- package/src/plugin/anthropic.ts +324 -0
- package/src/plugin/codex.ts +656 -0
- package/src/plugin/copilot.ts +161 -0
- package/src/plugin/google.ts +454 -0
- package/src/plugin/index.ts +30 -0
- package/src/provider/.gitkeep +0 -0
- package/src/provider/bundled.ts +59 -0
- package/src/provider/index.ts +249 -0
- package/src/provider/state.ts +163 -0
- package/src/storage/.gitkeep +0 -0
- package/src/storage/auto.ts +32 -0
- package/src/storage/file.ts +84 -0
- package/src/storage/index.ts +10 -0
- package/src/storage/memory.ts +23 -0
- package/src/types/.gitkeep +0 -0
- package/src/types/auth.ts +18 -0
- package/src/types/errors.ts +87 -0
- package/src/types/index.ts +26 -0
- package/src/types/model.ts +88 -0
- package/src/types/plugin.ts +49 -0
- package/src/types/provider.ts +48 -0
|
@@ -0,0 +1,357 @@
|
|
|
1
|
+
import { createLogger } from '../logger.js'
|
|
2
|
+
import { type StorageAdapter, createDefaultStorage } from '../storage/index.js'
|
|
3
|
+
import type { AuthCredential } from '../types/plugin.js'
|
|
4
|
+
import type { DiskScanResult, DiskScanner, ScanContext } from './scanners.js'
|
|
5
|
+
import { DEFAULT_SCANNERS, createNodeScanContext, runDiskScanners } from './scanners.js'
|
|
6
|
+
|
|
7
|
+
const log = createLogger('auth')
|
|
8
|
+
|
|
9
|
+
export interface DiscoveredCredential {
|
|
10
|
+
providerId: string
|
|
11
|
+
source: 'env' | 'disk' | 'auth'
|
|
12
|
+
key?: string
|
|
13
|
+
credential?: AuthCredential
|
|
14
|
+
location?: string
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
export interface DiscoverOptions {
|
|
18
|
+
scanners?: DiskScanner[]
|
|
19
|
+
scanContext?: ScanContext
|
|
20
|
+
skipDiskScan?: boolean
|
|
21
|
+
skipEnvScan?: boolean
|
|
22
|
+
persist?: boolean
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
export interface AuthStoreOptions {
|
|
26
|
+
storage?: StorageAdapter
|
|
27
|
+
data?: Record<string, AuthCredential>
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
export interface AuthStore {
|
|
31
|
+
all(): Promise<Record<string, AuthCredential>>
|
|
32
|
+
get(providerId: string): Promise<AuthCredential | null>
|
|
33
|
+
set(providerId: string, credential: AuthCredential): Promise<void>
|
|
34
|
+
remove(providerId: string): Promise<void>
|
|
35
|
+
discover(options?: DiscoverOptions): Promise<DiscoveredCredential[]>
|
|
36
|
+
getPreferred?(providerId: string, prefer: 'api' | 'oauth'): Promise<AuthCredential | null>
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
function pickBestCredential(creds: AuthCredential[], prefer: 'api' | 'oauth' = 'api'): AuthCredential | undefined {
|
|
40
|
+
if (creds.length === 0) return undefined
|
|
41
|
+
const preferred = creds.find((c) => c.type === prefer && c.key !== undefined)
|
|
42
|
+
if (preferred !== undefined) return preferred
|
|
43
|
+
const withKey = creds.find((c) => c.key !== undefined)
|
|
44
|
+
if (withKey !== undefined) return withKey
|
|
45
|
+
return creds[0]
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
export function createAuthStore(options?: AuthStoreOptions): AuthStore {
|
|
49
|
+
const externalData = options?.data
|
|
50
|
+
let storagePromise: Promise<StorageAdapter> | undefined
|
|
51
|
+
const discoveredCredentials = new Map<string, AuthCredential[]>()
|
|
52
|
+
|
|
53
|
+
log('auth store created, external=%s', externalData !== undefined)
|
|
54
|
+
|
|
55
|
+
function getStorage(): Promise<StorageAdapter> {
|
|
56
|
+
if (storagePromise === undefined) {
|
|
57
|
+
storagePromise = options?.storage ? Promise.resolve(options.storage) : createDefaultStorage()
|
|
58
|
+
}
|
|
59
|
+
return storagePromise
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
async function readAuthState(): Promise<Record<string, AuthCredential>> {
|
|
63
|
+
if (externalData !== undefined) {
|
|
64
|
+
log('using external data, skipping file read')
|
|
65
|
+
return { ...externalData }
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
const storage = await getStorage()
|
|
69
|
+
const raw = await storage.get(AUTH_STORE_KEY)
|
|
70
|
+
if (raw === null) {
|
|
71
|
+
log('auth store not found, returning empty store')
|
|
72
|
+
return {}
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
const parsed: unknown = JSON.parse(raw)
|
|
76
|
+
if (typeof parsed !== 'object' || parsed === null || Array.isArray(parsed)) {
|
|
77
|
+
log('auth store payload is malformed, returning empty store')
|
|
78
|
+
return {}
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
const normalized = normalizeAuthState(parsed as Record<string, unknown>)
|
|
82
|
+
log('read auth store with %d entries', Object.keys(normalized).length)
|
|
83
|
+
return normalized
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
async function writeAuthState(data: Record<string, AuthCredential>): Promise<void> {
|
|
87
|
+
if (externalData !== undefined) {
|
|
88
|
+
for (const [k, v] of Object.entries(data)) {
|
|
89
|
+
externalData[k] = v
|
|
90
|
+
}
|
|
91
|
+
for (const k of Object.keys(externalData)) {
|
|
92
|
+
if (!(k in data)) {
|
|
93
|
+
delete externalData[k]
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
log('updated external data, %d entries', Object.keys(externalData).length)
|
|
97
|
+
return
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
const content = JSON.stringify(data, null, 2)
|
|
101
|
+
const storage = await getStorage()
|
|
102
|
+
await storage.set(AUTH_STORE_KEY, content)
|
|
103
|
+
|
|
104
|
+
log('wrote auth store with %d entries', Object.keys(data).length)
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
function mergeWithDiscovered(fileData: Record<string, AuthCredential>): Record<string, AuthCredential> {
|
|
108
|
+
if (discoveredCredentials.size === 0) return fileData
|
|
109
|
+
const merged = { ...fileData }
|
|
110
|
+
for (const [pid, creds] of discoveredCredentials) {
|
|
111
|
+
if (merged[pid] === undefined || !merged[pid].key) {
|
|
112
|
+
const best = pickBestCredential(creds, 'api')
|
|
113
|
+
if (best !== undefined) {
|
|
114
|
+
merged[pid] = best
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
log(
|
|
119
|
+
'merged %d discovered credentials with %d file entries',
|
|
120
|
+
discoveredCredentials.size,
|
|
121
|
+
Object.keys(fileData).length
|
|
122
|
+
)
|
|
123
|
+
return merged
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
function pushDiscoveredCredential(providerId: string, credential: AuthCredential): void {
|
|
127
|
+
const arr = discoveredCredentials.get(providerId) ?? []
|
|
128
|
+
arr.push(credential)
|
|
129
|
+
discoveredCredentials.set(providerId, arr)
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
function pushDiscoveredResultOnce(
|
|
133
|
+
seen: Set<string>,
|
|
134
|
+
results: DiscoveredCredential[],
|
|
135
|
+
result: DiscoveredCredential
|
|
136
|
+
): boolean {
|
|
137
|
+
if (seen.has(result.providerId)) return false
|
|
138
|
+
seen.add(result.providerId)
|
|
139
|
+
results.push(result)
|
|
140
|
+
return true
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
function buildCredentialFromEnv(envVar: string, key: string): AuthCredential {
|
|
144
|
+
return buildCredential({ type: 'api', key, location: `env:${envVar}` })
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
function buildCredentialFromDisk(result: DiskScanResult): AuthCredential | undefined {
|
|
148
|
+
if (result.key === undefined) return undefined
|
|
149
|
+
return buildCredential({
|
|
150
|
+
type: result.credentialType ?? 'api',
|
|
151
|
+
key: result.key,
|
|
152
|
+
location: result.source,
|
|
153
|
+
refresh: result.refresh,
|
|
154
|
+
accountId: result.accountId,
|
|
155
|
+
expires: result.expires,
|
|
156
|
+
})
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
return {
|
|
160
|
+
async all(): Promise<Record<string, AuthCredential>> {
|
|
161
|
+
return mergeWithDiscovered(await readAuthState())
|
|
162
|
+
},
|
|
163
|
+
|
|
164
|
+
async get(providerId: string): Promise<AuthCredential | null> {
|
|
165
|
+
const store = mergeWithDiscovered(await readAuthState())
|
|
166
|
+
const credential = store[providerId]
|
|
167
|
+
if (credential === undefined) {
|
|
168
|
+
log('get(%s): not found', providerId)
|
|
169
|
+
return null
|
|
170
|
+
}
|
|
171
|
+
log('get(%s): found type=%s', providerId, credential.type)
|
|
172
|
+
return credential
|
|
173
|
+
},
|
|
174
|
+
|
|
175
|
+
async set(providerId: string, credential: AuthCredential): Promise<void> {
|
|
176
|
+
const store = await readAuthState()
|
|
177
|
+
store[providerId] = credential
|
|
178
|
+
await writeAuthState(store)
|
|
179
|
+
log('set(%s): type=%s', providerId, credential.type)
|
|
180
|
+
},
|
|
181
|
+
|
|
182
|
+
async remove(providerId: string): Promise<void> {
|
|
183
|
+
const store = await readAuthState()
|
|
184
|
+
if (!(providerId in store)) {
|
|
185
|
+
log('remove(%s): not found, no-op', providerId)
|
|
186
|
+
return
|
|
187
|
+
}
|
|
188
|
+
delete store[providerId]
|
|
189
|
+
await writeAuthState(store)
|
|
190
|
+
log('remove(%s): done', providerId)
|
|
191
|
+
},
|
|
192
|
+
|
|
193
|
+
async discover(discoverOptions?: DiscoverOptions): Promise<DiscoveredCredential[]> {
|
|
194
|
+
const results: DiscoveredCredential[] = []
|
|
195
|
+
const seen = new Set<string>()
|
|
196
|
+
|
|
197
|
+
if (!discoverOptions?.skipEnvScan) {
|
|
198
|
+
for (const [envVar, providerId] of ENV_HINTS) {
|
|
199
|
+
const value = process.env[envVar]
|
|
200
|
+
if (value) {
|
|
201
|
+
pushDiscoveredCredential(providerId, buildCredentialFromEnv(envVar, value))
|
|
202
|
+
pushDiscoveredResultOnce(seen, results, { providerId, source: 'env', key: value })
|
|
203
|
+
log('discover: %s [api] from env:%s', providerId, envVar)
|
|
204
|
+
}
|
|
205
|
+
}
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
if (!discoverOptions?.skipDiskScan) {
|
|
209
|
+
const scanners = discoverOptions?.scanners ?? DEFAULT_SCANNERS
|
|
210
|
+
const ctx = discoverOptions?.scanContext ?? createNodeScanContext()
|
|
211
|
+
const diskResults = await runDiskScanners(scanners, ctx)
|
|
212
|
+
|
|
213
|
+
for (const disk of diskResults) {
|
|
214
|
+
const credType = disk.credentialType ?? 'api'
|
|
215
|
+
pushDiscoveredResultOnce(seen, results, {
|
|
216
|
+
providerId: disk.providerId,
|
|
217
|
+
source: 'disk',
|
|
218
|
+
key: disk.key,
|
|
219
|
+
location: disk.source,
|
|
220
|
+
})
|
|
221
|
+
const cred = buildCredentialFromDisk(disk)
|
|
222
|
+
if (cred !== undefined) {
|
|
223
|
+
pushDiscoveredCredential(disk.providerId, cred)
|
|
224
|
+
log('discover: %s [%s] from %s', disk.providerId, credType, disk.source)
|
|
225
|
+
}
|
|
226
|
+
}
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
if (discoverOptions?.persist === true) {
|
|
230
|
+
const persistedCount = await persistDiscoveredCredentials()
|
|
231
|
+
log('discover: persisted %d providers to auth store', persistedCount)
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
let authData: Record<string, AuthCredential>
|
|
235
|
+
try {
|
|
236
|
+
authData = await readAuthState()
|
|
237
|
+
} catch (err: unknown) {
|
|
238
|
+
log('discover: failed to read auth store: %s', err instanceof Error ? err.message : String(err))
|
|
239
|
+
authData = {}
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
for (const [providerId, credential] of Object.entries(authData)) {
|
|
243
|
+
const added = pushDiscoveredResultOnce(seen, results, { providerId, source: 'auth', credential })
|
|
244
|
+
if (added) log('discover: %s via auth store', providerId)
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
log('discover: complete, %d providers found', results.length)
|
|
248
|
+
return results
|
|
249
|
+
},
|
|
250
|
+
|
|
251
|
+
async getPreferred(providerId: string, prefer: 'api' | 'oauth'): Promise<AuthCredential | null> {
|
|
252
|
+
const creds = discoveredCredentials.get(providerId)
|
|
253
|
+
if (creds !== undefined && creds.length > 0) {
|
|
254
|
+
const best = pickBestCredential(creds, prefer)
|
|
255
|
+
if (best !== undefined) return best
|
|
256
|
+
}
|
|
257
|
+
const store = mergeWithDiscovered(await readAuthState())
|
|
258
|
+
const credential = store[providerId]
|
|
259
|
+
return credential ?? null
|
|
260
|
+
},
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
async function persistDiscoveredCredentials(): Promise<number> {
|
|
264
|
+
if (discoveredCredentials.size === 0) return 0
|
|
265
|
+
|
|
266
|
+
const store = await readAuthState()
|
|
267
|
+
let changed = 0
|
|
268
|
+
|
|
269
|
+
for (const [providerId, creds] of discoveredCredentials) {
|
|
270
|
+
const existing = store[providerId]
|
|
271
|
+
if (existing?.key) {
|
|
272
|
+
continue
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
const best = pickBestCredential(creds, 'api')
|
|
276
|
+
if (best === undefined) {
|
|
277
|
+
continue
|
|
278
|
+
}
|
|
279
|
+
|
|
280
|
+
store[providerId] = best
|
|
281
|
+
changed += 1
|
|
282
|
+
}
|
|
283
|
+
|
|
284
|
+
if (changed > 0) {
|
|
285
|
+
await writeAuthState(store)
|
|
286
|
+
}
|
|
287
|
+
|
|
288
|
+
return changed
|
|
289
|
+
}
|
|
290
|
+
}
|
|
291
|
+
|
|
292
|
+
const AUTH_STORE_KEY = 'auth:store'
|
|
293
|
+
|
|
294
|
+
interface CredentialBuildInput {
|
|
295
|
+
type: 'api' | 'oauth' | 'wellknown'
|
|
296
|
+
key?: string
|
|
297
|
+
location?: string
|
|
298
|
+
refresh?: string
|
|
299
|
+
accountId?: string
|
|
300
|
+
expires?: number
|
|
301
|
+
}
|
|
302
|
+
|
|
303
|
+
function buildCredential(input: CredentialBuildInput): AuthCredential {
|
|
304
|
+
const credential: AuthCredential = {
|
|
305
|
+
type: input.type,
|
|
306
|
+
key: input.key,
|
|
307
|
+
location: input.location,
|
|
308
|
+
}
|
|
309
|
+
|
|
310
|
+
if (input.refresh !== undefined) credential.refresh = input.refresh
|
|
311
|
+
if (input.accountId !== undefined) credential.accountId = input.accountId
|
|
312
|
+
if (input.expires !== undefined) credential.expires = input.expires
|
|
313
|
+
|
|
314
|
+
return credential
|
|
315
|
+
}
|
|
316
|
+
|
|
317
|
+
function normalizeAuthState(input: Record<string, unknown>): Record<string, AuthCredential> {
|
|
318
|
+
const normalized: Record<string, AuthCredential> = {}
|
|
319
|
+
|
|
320
|
+
for (const [providerId, rawCredential] of Object.entries(input)) {
|
|
321
|
+
const credential = normalizeCredential(rawCredential)
|
|
322
|
+
if (credential !== undefined) {
|
|
323
|
+
normalized[providerId] = credential
|
|
324
|
+
}
|
|
325
|
+
}
|
|
326
|
+
|
|
327
|
+
return normalized
|
|
328
|
+
}
|
|
329
|
+
|
|
330
|
+
function normalizeCredential(rawCredential: unknown): AuthCredential | undefined {
|
|
331
|
+
if (typeof rawCredential !== 'object' || rawCredential === null || Array.isArray(rawCredential)) {
|
|
332
|
+
return undefined
|
|
333
|
+
}
|
|
334
|
+
|
|
335
|
+
const typed = rawCredential as Record<string, unknown>
|
|
336
|
+
const type =
|
|
337
|
+
typed.type === 'api' || typed.type === 'oauth' || typed.type === 'wellknown' ? typed.type : ('api' as const)
|
|
338
|
+
|
|
339
|
+
const key = typeof typed.key === 'string' && typed.key.length > 0 ? typed.key : undefined
|
|
340
|
+
return {
|
|
341
|
+
...typed,
|
|
342
|
+
type,
|
|
343
|
+
key,
|
|
344
|
+
} as AuthCredential
|
|
345
|
+
}
|
|
346
|
+
|
|
347
|
+
const ENV_HINTS: Array<[string, string]> = [
|
|
348
|
+
['ANTHROPIC_API_KEY', 'anthropic'],
|
|
349
|
+
['OPENAI_API_KEY', 'openai'],
|
|
350
|
+
['GOOGLE_GENERATIVE_AI_API_KEY', 'google'],
|
|
351
|
+
['GOOGLE_API_KEY', 'google'],
|
|
352
|
+
['AZURE_API_KEY', 'azure'],
|
|
353
|
+
['XAI_API_KEY', 'xai'],
|
|
354
|
+
['MISTRAL_API_KEY', 'mistral'],
|
|
355
|
+
['GROQ_API_KEY', 'groq'],
|
|
356
|
+
['OPENROUTER_API_KEY', 'openrouter'],
|
|
357
|
+
]
|
|
File without changes
|
|
@@ -0,0 +1,302 @@
|
|
|
1
|
+
import { createLogger } from '../logger.js'
|
|
2
|
+
import type { ModelDefinition } from '../types/model.js'
|
|
3
|
+
import { mapModelsDevProvider, mapModelsDevProviderMetadata } from './mapper.js'
|
|
4
|
+
import { mergeCatalogData, mergeModelDefinitions } from './merger.js'
|
|
5
|
+
|
|
6
|
+
const DEFAULT_REMOTE_URL = 'https://models.dev/api.json'
|
|
7
|
+
const DEFAULT_TIMEOUT_MS = 10_000
|
|
8
|
+
|
|
9
|
+
const log = createLogger('catalog')
|
|
10
|
+
|
|
11
|
+
type CatalogDataSource = Record<string, unknown>
|
|
12
|
+
type ProviderModelMap = Map<string, Map<string, ModelDefinition>>
|
|
13
|
+
type FetchLike = (
|
|
14
|
+
url: string,
|
|
15
|
+
init?: { signal?: unknown }
|
|
16
|
+
) => Promise<{ ok: boolean; status: number; json(): Promise<unknown> }>
|
|
17
|
+
|
|
18
|
+
export interface CatalogProvider {
|
|
19
|
+
id: string
|
|
20
|
+
name: string
|
|
21
|
+
env?: string[]
|
|
22
|
+
api?: string
|
|
23
|
+
doc?: string
|
|
24
|
+
bundledProvider?: string
|
|
25
|
+
baseURL?: string
|
|
26
|
+
headers?: Record<string, string>
|
|
27
|
+
options?: Record<string, unknown>
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
export interface ExtendModelConfig {
|
|
31
|
+
name?: string
|
|
32
|
+
modalities?: {
|
|
33
|
+
input: Array<'text' | 'image' | 'audio' | 'video' | 'pdf'>
|
|
34
|
+
output: Array<'text' | 'image' | 'audio'>
|
|
35
|
+
}
|
|
36
|
+
limit?: { context: number; output: number }
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
export interface ExtendProviderConfig {
|
|
40
|
+
name: string
|
|
41
|
+
env?: string[]
|
|
42
|
+
bundledProvider?: string
|
|
43
|
+
baseURL?: string
|
|
44
|
+
headers?: Record<string, string>
|
|
45
|
+
options?: Record<string, unknown>
|
|
46
|
+
models?: Record<string, ExtendModelConfig>
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
export interface ExtendConfig {
|
|
50
|
+
providers?: Record<string, ExtendProviderConfig>
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
export interface CatalogOptions {
|
|
54
|
+
snapshot?: Record<string, unknown>
|
|
55
|
+
remote?: {
|
|
56
|
+
url?: string
|
|
57
|
+
timeoutMs?: number
|
|
58
|
+
fetch?: FetchLike
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
export interface RefreshResult {
|
|
63
|
+
success: boolean
|
|
64
|
+
updatedProviders: string[]
|
|
65
|
+
newModels: number
|
|
66
|
+
error?: Error
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
export class Catalog {
|
|
70
|
+
private readonly remoteOptions: {
|
|
71
|
+
readonly url: string
|
|
72
|
+
readonly timeoutMs: number
|
|
73
|
+
readonly fetch?: FetchLike
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
private readonly snapshotData: CatalogDataSource
|
|
77
|
+
private remoteData: CatalogDataSource = {}
|
|
78
|
+
private readonly providers = new Map<string, CatalogProvider>()
|
|
79
|
+
private modelsByProvider: ProviderModelMap = new Map()
|
|
80
|
+
private refreshInFlight: Promise<RefreshResult> | null = null
|
|
81
|
+
private readonly extendedProviders = new Map<string, CatalogProvider>()
|
|
82
|
+
private readonly extendedModels = new Map<string, Map<string, Partial<ModelDefinition>>>()
|
|
83
|
+
|
|
84
|
+
constructor(options: CatalogOptions = {}) {
|
|
85
|
+
this.snapshotData = options.snapshot ?? {}
|
|
86
|
+
this.remoteOptions = {
|
|
87
|
+
url: options.remote?.url ?? DEFAULT_REMOTE_URL,
|
|
88
|
+
timeoutMs: options.remote?.timeoutMs ?? DEFAULT_TIMEOUT_MS,
|
|
89
|
+
fetch: options.remote?.fetch,
|
|
90
|
+
}
|
|
91
|
+
this.applyProviderMetadata(this.snapshotData, {})
|
|
92
|
+
log('initialized with %d providers from snapshot', this.providers.size)
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
getProvider(id: string): CatalogProvider | undefined {
|
|
96
|
+
return this.providers.get(id)
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
listProviders(): CatalogProvider[] {
|
|
100
|
+
return [...this.providers.values()]
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
getModel(providerId: string, modelId: string): ModelDefinition | undefined {
|
|
104
|
+
this.ensureProviderModelsLoaded(providerId)
|
|
105
|
+
return this.modelsByProvider.get(providerId)?.get(modelId)
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
listModels(providerId?: string): ModelDefinition[] {
|
|
109
|
+
if (providerId !== undefined) {
|
|
110
|
+
this.ensureProviderModelsLoaded(providerId)
|
|
111
|
+
return [...(this.modelsByProvider.get(providerId)?.values() ?? [])]
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
const results: ModelDefinition[] = []
|
|
115
|
+
for (const pid of this.providers.keys()) {
|
|
116
|
+
this.ensureProviderModelsLoaded(pid)
|
|
117
|
+
const models = this.modelsByProvider.get(pid)
|
|
118
|
+
if (models) {
|
|
119
|
+
results.push(...models.values())
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
return results
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
enrichModel(providerId: string, modelId: string, partial: Partial<ModelDefinition>): ModelDefinition {
|
|
126
|
+
const catalogModel = this.getModel(providerId, modelId)
|
|
127
|
+
const partialWithId = { ...partial, modelId }
|
|
128
|
+
if (!catalogModel) {
|
|
129
|
+
return partialWithId as ModelDefinition
|
|
130
|
+
}
|
|
131
|
+
return mergeModelDefinitions(catalogModel, partialWithId)
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
extend(config: ExtendConfig): void {
|
|
135
|
+
if (!config.providers) return
|
|
136
|
+
|
|
137
|
+
for (const [providerId, providerConfig] of Object.entries(config.providers)) {
|
|
138
|
+
const existingProvider = this.providers.get(providerId)
|
|
139
|
+
const provider: CatalogProvider = {
|
|
140
|
+
id: providerId,
|
|
141
|
+
name: providerConfig.name,
|
|
142
|
+
...(providerConfig.env !== undefined ? { env: providerConfig.env } : {}),
|
|
143
|
+
...(existingProvider?.api !== undefined ? { api: existingProvider.api } : {}),
|
|
144
|
+
...(existingProvider?.doc !== undefined ? { doc: existingProvider.doc } : {}),
|
|
145
|
+
...(providerConfig.bundledProvider !== undefined ? { bundledProvider: providerConfig.bundledProvider } : {}),
|
|
146
|
+
...(providerConfig.baseURL !== undefined ? { baseURL: providerConfig.baseURL } : {}),
|
|
147
|
+
...(providerConfig.headers !== undefined ? { headers: providerConfig.headers } : {}),
|
|
148
|
+
...(providerConfig.options !== undefined ? { options: providerConfig.options } : {}),
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
this.extendedProviders.set(providerId, provider)
|
|
152
|
+
this.providers.set(providerId, provider)
|
|
153
|
+
|
|
154
|
+
if (providerConfig.models) {
|
|
155
|
+
const modelOverrides = this.extendedModels.get(providerId) ?? new Map<string, Partial<ModelDefinition>>()
|
|
156
|
+
|
|
157
|
+
for (const [modelId, modelConfig] of Object.entries(providerConfig.models)) {
|
|
158
|
+
const partial: Partial<ModelDefinition> = { modelId }
|
|
159
|
+
if (modelConfig.name !== undefined) partial.name = modelConfig.name
|
|
160
|
+
if (modelConfig.modalities !== undefined) partial.modalities = modelConfig.modalities
|
|
161
|
+
if (modelConfig.limit !== undefined) partial.limit = modelConfig.limit
|
|
162
|
+
modelOverrides.set(modelId, partial)
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
this.extendedModels.set(providerId, modelOverrides)
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
this.modelsByProvider.delete(providerId)
|
|
169
|
+
}
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
refresh(): Promise<RefreshResult> {
|
|
173
|
+
if (this.refreshInFlight) {
|
|
174
|
+
return this.refreshInFlight
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
const task = this.refreshInternal().finally(() => {
|
|
178
|
+
this.refreshInFlight = null
|
|
179
|
+
})
|
|
180
|
+
|
|
181
|
+
this.refreshInFlight = task
|
|
182
|
+
return task
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
private async refreshInternal(): Promise<RefreshResult> {
|
|
186
|
+
try {
|
|
187
|
+
log('refresh: fetching from %s', this.remoteOptions.url)
|
|
188
|
+
const remoteData = await this.fetchRemoteData()
|
|
189
|
+
this.remoteData = remoteData
|
|
190
|
+
const { updatedProviders } = this.applyProviderMetadata(this.snapshotData, this.remoteData)
|
|
191
|
+
this.modelsByProvider.clear()
|
|
192
|
+
log('refresh: updated %d providers', updatedProviders.length)
|
|
193
|
+
return { success: true, updatedProviders, newModels: 0 }
|
|
194
|
+
} catch (error) {
|
|
195
|
+
log('refresh: failed — %s', error instanceof Error ? error.message : String(error))
|
|
196
|
+
return {
|
|
197
|
+
success: false,
|
|
198
|
+
updatedProviders: [],
|
|
199
|
+
newModels: 0,
|
|
200
|
+
error: error instanceof Error ? error : new Error(String(error)),
|
|
201
|
+
}
|
|
202
|
+
}
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
private async fetchRemoteData(): Promise<CatalogDataSource> {
|
|
206
|
+
const fetchFn = this.remoteOptions.fetch ?? this.resolveGlobalFetch()
|
|
207
|
+
if (!fetchFn) throw new Error('No fetch implementation available')
|
|
208
|
+
|
|
209
|
+
const response = await fetchFn(this.remoteOptions.url)
|
|
210
|
+
if (!response.ok) throw new Error(`Failed to fetch remote catalog: HTTP ${response.status}`)
|
|
211
|
+
|
|
212
|
+
const payload = await response.json()
|
|
213
|
+
if (payload && typeof payload === 'object') return payload as CatalogDataSource
|
|
214
|
+
throw new Error('Invalid remote catalog payload')
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
private resolveGlobalFetch(): FetchLike | undefined {
|
|
218
|
+
const maybeFetch = (globalThis as Record<string, unknown>).fetch
|
|
219
|
+
return typeof maybeFetch === 'function' ? (maybeFetch as FetchLike) : undefined
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
private applyProviderMetadata(
|
|
223
|
+
snapshotRaw: CatalogDataSource,
|
|
224
|
+
remoteRaw: CatalogDataSource
|
|
225
|
+
): { updatedProviders: string[] } {
|
|
226
|
+
const snapshotProviders = this.mapProviderMetadata(snapshotRaw)
|
|
227
|
+
const remoteProviders = this.mapProviderMetadata(remoteRaw)
|
|
228
|
+
const allProviderIds = new Set<string>([...snapshotProviders.keys(), ...remoteProviders.keys()])
|
|
229
|
+
|
|
230
|
+
this.providers.clear()
|
|
231
|
+
for (const providerId of allProviderIds) {
|
|
232
|
+
const provider = remoteProviders.get(providerId) ?? snapshotProviders.get(providerId)
|
|
233
|
+
if (provider) this.providers.set(providerId, provider)
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
for (const [providerId, provider] of this.extendedProviders.entries()) {
|
|
237
|
+
this.providers.set(providerId, provider)
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
return { updatedProviders: [...remoteProviders.keys()] }
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
private mapProviderMetadata(data: CatalogDataSource): Map<string, CatalogProvider> {
|
|
244
|
+
const result = new Map<string, CatalogProvider>()
|
|
245
|
+
for (const [providerId, rawProvider] of Object.entries(data)) {
|
|
246
|
+
if (providerId.startsWith('_')) continue
|
|
247
|
+
result.set(providerId, mapModelsDevProviderMetadata(providerId, rawProvider))
|
|
248
|
+
}
|
|
249
|
+
return result
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
private providerRaw(data: CatalogDataSource, providerId: string): unknown {
|
|
253
|
+
const value = data[providerId]
|
|
254
|
+
return value && typeof value === 'object' ? value : undefined
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
private mapProviderModelsFromRaw(
|
|
258
|
+
providerId: string,
|
|
259
|
+
raw: unknown,
|
|
260
|
+
provenance: 'snapshot' | 'remote'
|
|
261
|
+
): Map<string, ModelDefinition> {
|
|
262
|
+
if (!raw || typeof raw !== 'object') return new Map<string, ModelDefinition>()
|
|
263
|
+
const mapped = mapModelsDevProvider(providerId, raw, provenance)
|
|
264
|
+
const byId = new Map<string, ModelDefinition>()
|
|
265
|
+
for (const model of mapped.models) {
|
|
266
|
+
byId.set(model.modelId, model)
|
|
267
|
+
}
|
|
268
|
+
return byId
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
private ensureProviderModelsLoaded(providerId: string): void {
|
|
272
|
+
if (this.modelsByProvider.has(providerId)) return
|
|
273
|
+
|
|
274
|
+
const snapshotModels = this.mapProviderModelsFromRaw(
|
|
275
|
+
providerId,
|
|
276
|
+
this.providerRaw(this.snapshotData, providerId),
|
|
277
|
+
'snapshot'
|
|
278
|
+
)
|
|
279
|
+
const remoteModels = this.mapProviderModelsFromRaw(
|
|
280
|
+
providerId,
|
|
281
|
+
this.providerRaw(this.remoteData, providerId),
|
|
282
|
+
'remote'
|
|
283
|
+
)
|
|
284
|
+
const extendedOverrides = this.extendedModels.get(providerId) ?? new Map<string, Partial<ModelDefinition>>()
|
|
285
|
+
|
|
286
|
+
const merged = mergeCatalogData(snapshotModels, remoteModels, extendedOverrides)
|
|
287
|
+
|
|
288
|
+
for (const [modelId, partial] of extendedOverrides.entries()) {
|
|
289
|
+
if (!merged.has(modelId)) {
|
|
290
|
+
const base: ModelDefinition = {
|
|
291
|
+
modelId,
|
|
292
|
+
modalities: { input: ['text'], output: ['text'] },
|
|
293
|
+
limit: { context: 0, output: 0 },
|
|
294
|
+
provenance: 'user-override',
|
|
295
|
+
}
|
|
296
|
+
merged.set(modelId, mergeModelDefinitions(base, partial))
|
|
297
|
+
}
|
|
298
|
+
}
|
|
299
|
+
|
|
300
|
+
this.modelsByProvider.set(providerId, merged)
|
|
301
|
+
}
|
|
302
|
+
}
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
import type { CatalogOptions } from './catalog.js'
|
|
2
|
+
import { Catalog } from './catalog.js'
|
|
3
|
+
|
|
4
|
+
export type {
|
|
5
|
+
CatalogOptions,
|
|
6
|
+
CatalogProvider,
|
|
7
|
+
ExtendConfig,
|
|
8
|
+
ExtendModelConfig,
|
|
9
|
+
ExtendProviderConfig,
|
|
10
|
+
RefreshResult,
|
|
11
|
+
} from './catalog.js'
|
|
12
|
+
|
|
13
|
+
export { Catalog }
|
|
14
|
+
|
|
15
|
+
export function createCatalog(options: CatalogOptions = {}): Catalog {
|
|
16
|
+
return new Catalog(options)
|
|
17
|
+
}
|