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,59 @@
|
|
|
1
|
+
import type { LanguageModelV3 } from '@ai-sdk/provider'
|
|
2
|
+
import { createLogger } from '../logger.js'
|
|
3
|
+
|
|
4
|
+
const log = createLogger('provider:bundled')
|
|
5
|
+
|
|
6
|
+
export interface ProviderInstance {
|
|
7
|
+
languageModel(modelId: string): LanguageModelV3
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
export type ProviderFactory = (options: Record<string, unknown>) => ProviderInstance
|
|
11
|
+
|
|
12
|
+
const PROVIDER_LOADERS: Record<string, () => Promise<ProviderFactory>> = {
|
|
13
|
+
'@ai-sdk/anthropic': () => import('@ai-sdk/anthropic').then((m) => m.createAnthropic as unknown as ProviderFactory),
|
|
14
|
+
'@ai-sdk/openai': () => import('@ai-sdk/openai').then((m) => m.createOpenAI as unknown as ProviderFactory),
|
|
15
|
+
'@ai-sdk/google': () =>
|
|
16
|
+
import('@ai-sdk/google').then((m) => m.createGoogleGenerativeAI as unknown as ProviderFactory),
|
|
17
|
+
'@ai-sdk/google-vertex': () =>
|
|
18
|
+
import('@ai-sdk/google-vertex').then((m) => m.createVertex as unknown as ProviderFactory),
|
|
19
|
+
'@ai-sdk/amazon-bedrock': () =>
|
|
20
|
+
import('@ai-sdk/amazon-bedrock').then((m) => m.createAmazonBedrock as unknown as ProviderFactory),
|
|
21
|
+
'@ai-sdk/azure': () => import('@ai-sdk/azure').then((m) => m.createAzure as unknown as ProviderFactory),
|
|
22
|
+
'@ai-sdk/openai-compatible': () =>
|
|
23
|
+
import('@ai-sdk/openai-compatible').then((m) => m.createOpenAICompatible as unknown as ProviderFactory),
|
|
24
|
+
'@ai-sdk/xai': () => import('@ai-sdk/xai').then((m) => m.createXai as unknown as ProviderFactory),
|
|
25
|
+
'@ai-sdk/mistral': () => import('@ai-sdk/mistral').then((m) => m.createMistral as unknown as ProviderFactory),
|
|
26
|
+
'@ai-sdk/groq': () => import('@ai-sdk/groq').then((m) => m.createGroq as unknown as ProviderFactory),
|
|
27
|
+
'@openrouter/ai-sdk-provider': () =>
|
|
28
|
+
import('@openrouter/ai-sdk-provider').then((m) => m.createOpenRouter as unknown as ProviderFactory),
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
const loadedProviders = new Map<string, ProviderFactory>()
|
|
32
|
+
const unavailableProviders = new Set<string>()
|
|
33
|
+
|
|
34
|
+
export async function loadProvider(packageName: string): Promise<ProviderFactory | undefined> {
|
|
35
|
+
if (loadedProviders.has(packageName)) return loadedProviders.get(packageName)
|
|
36
|
+
if (unavailableProviders.has(packageName)) return undefined
|
|
37
|
+
|
|
38
|
+
const loader = PROVIDER_LOADERS[packageName]
|
|
39
|
+
if (loader === undefined) return undefined
|
|
40
|
+
|
|
41
|
+
try {
|
|
42
|
+
const factory = await loader()
|
|
43
|
+
loadedProviders.set(packageName, factory)
|
|
44
|
+
log('loaded provider package: %s', packageName)
|
|
45
|
+
return factory
|
|
46
|
+
} catch {
|
|
47
|
+
unavailableProviders.add(packageName)
|
|
48
|
+
log('provider package not available: %s (not installed)', packageName)
|
|
49
|
+
return undefined
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
export async function isProviderInstalled(packageName: string): Promise<boolean> {
|
|
54
|
+
return (await loadProvider(packageName)) !== undefined
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
export function getAllProviderPackages(): string[] {
|
|
58
|
+
return Object.keys(PROVIDER_LOADERS)
|
|
59
|
+
}
|
|
@@ -0,0 +1,249 @@
|
|
|
1
|
+
import type { LanguageModelV3 } from '@ai-sdk/provider'
|
|
2
|
+
import type { AuthStore } from '../auth/store.js'
|
|
3
|
+
import { createAuthStore } from '../auth/store.js'
|
|
4
|
+
import type { CatalogProvider, ExtendConfig } from '../catalog/catalog.js'
|
|
5
|
+
import { Catalog } from '../catalog/catalog.js'
|
|
6
|
+
import { createLogger } from '../logger.js'
|
|
7
|
+
import { anthropicPlugin } from '../plugin/anthropic.js'
|
|
8
|
+
import { codexPlugin } from '../plugin/codex.js'
|
|
9
|
+
import { copilotPlugin } from '../plugin/copilot.js'
|
|
10
|
+
import { googlePlugin } from '../plugin/google.js'
|
|
11
|
+
import { registerPlugin } from '../plugin/index.js'
|
|
12
|
+
import type { ModelDefinition } from '../types/model.js'
|
|
13
|
+
import type { ProviderUserConfig } from '../types/provider.js'
|
|
14
|
+
import { isProviderInstalled, loadProvider } from './bundled.js'
|
|
15
|
+
import { buildProviderState } from './state.js'
|
|
16
|
+
|
|
17
|
+
export type { ProviderInstance, ProviderFactory } from './bundled.js'
|
|
18
|
+
export { loadProvider, isProviderInstalled, getAllProviderPackages } from './bundled.js'
|
|
19
|
+
|
|
20
|
+
const log = createLogger('provider')
|
|
21
|
+
|
|
22
|
+
const DEFAULT_PROVIDERS: Record<string, { name: string; env: string[]; bundledProvider: string }> = {
|
|
23
|
+
anthropic: { name: 'Anthropic', env: ['ANTHROPIC_API_KEY'], bundledProvider: '@ai-sdk/anthropic' },
|
|
24
|
+
openai: { name: 'OpenAI', env: ['OPENAI_API_KEY'], bundledProvider: '@ai-sdk/openai' },
|
|
25
|
+
google: {
|
|
26
|
+
name: 'Google AI',
|
|
27
|
+
env: ['GOOGLE_GENERATIVE_AI_API_KEY', 'GOOGLE_API_KEY'],
|
|
28
|
+
bundledProvider: '@ai-sdk/google',
|
|
29
|
+
},
|
|
30
|
+
'google-vertex': { name: 'Google Vertex AI', env: [], bundledProvider: '@ai-sdk/google-vertex' },
|
|
31
|
+
'amazon-bedrock': { name: 'Amazon Bedrock', env: [], bundledProvider: '@ai-sdk/amazon-bedrock' },
|
|
32
|
+
azure: { name: 'Azure OpenAI', env: ['AZURE_API_KEY'], bundledProvider: '@ai-sdk/azure' },
|
|
33
|
+
xai: { name: 'xAI', env: ['XAI_API_KEY'], bundledProvider: '@ai-sdk/xai' },
|
|
34
|
+
mistral: { name: 'Mistral', env: ['MISTRAL_API_KEY'], bundledProvider: '@ai-sdk/mistral' },
|
|
35
|
+
groq: { name: 'Groq', env: ['GROQ_API_KEY'], bundledProvider: '@ai-sdk/groq' },
|
|
36
|
+
openrouter: { name: 'OpenRouter', env: ['OPENROUTER_API_KEY'], bundledProvider: '@openrouter/ai-sdk-provider' },
|
|
37
|
+
'github-copilot': { name: 'GitHub Copilot', env: [], bundledProvider: '@ai-sdk/openai-compatible' },
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
function resolveBundledProviderKey(providerId: string, catalogProvider?: CatalogProvider): string | undefined {
|
|
41
|
+
if (catalogProvider?.bundledProvider !== undefined) return catalogProvider.bundledProvider
|
|
42
|
+
return DEFAULT_PROVIDERS[providerId]?.bundledProvider
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
export interface ProviderStoreConfig {
|
|
46
|
+
userConfig?: Record<string, ProviderUserConfig>
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
export interface ProviderListOptions {
|
|
50
|
+
includeUnavailable?: boolean
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
export interface ModelListOptions {
|
|
54
|
+
includeUnavailable?: boolean
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
export interface GetModelOptions {
|
|
58
|
+
includeUnavailable?: boolean
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
export interface ProviderStore {
|
|
62
|
+
getLanguageModel(providerId: string, modelId: string): Promise<LanguageModelV3>
|
|
63
|
+
extend(config: ExtendConfig): void
|
|
64
|
+
listProviders(options?: ProviderListOptions): Promise<CatalogProvider[]>
|
|
65
|
+
listModels(providerId?: string, options?: ModelListOptions): Promise<ModelDefinition[]>
|
|
66
|
+
getModel(providerId: string, modelId: string, options?: GetModelOptions): Promise<ModelDefinition | undefined>
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
export function createProviderStore(authStore: AuthStore, config?: ProviderStoreConfig): ProviderStore {
|
|
70
|
+
const catalog = new Catalog()
|
|
71
|
+
registerPlugin(copilotPlugin)
|
|
72
|
+
registerPlugin(codexPlugin)
|
|
73
|
+
registerPlugin(googlePlugin)
|
|
74
|
+
registerPlugin(anthropicPlugin)
|
|
75
|
+
catalog.extend({
|
|
76
|
+
providers: Object.fromEntries(
|
|
77
|
+
Object.entries(DEFAULT_PROVIDERS).map(([id, p]) => [
|
|
78
|
+
id,
|
|
79
|
+
{ name: p.name, env: p.env, bundledProvider: p.bundledProvider },
|
|
80
|
+
])
|
|
81
|
+
),
|
|
82
|
+
})
|
|
83
|
+
const userConfig = config?.userConfig
|
|
84
|
+
let stateCache: Promise<Record<string, import('./state.js').ProviderState>> | null = null
|
|
85
|
+
let catalogRefreshTask: Promise<void> | null = null
|
|
86
|
+
|
|
87
|
+
function invalidateState() {
|
|
88
|
+
stateCache = null
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
function getState() {
|
|
92
|
+
if (stateCache === null) {
|
|
93
|
+
log('initializing provider state')
|
|
94
|
+
stateCache = buildProviderState({ catalog, authStore, userConfig })
|
|
95
|
+
}
|
|
96
|
+
return stateCache
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
async function ensureCatalogEnriched(): Promise<void> {
|
|
100
|
+
if (catalogRefreshTask === null) {
|
|
101
|
+
catalogRefreshTask = (async () => {
|
|
102
|
+
const result = await catalog.refresh()
|
|
103
|
+
if (!result.success) {
|
|
104
|
+
log('catalog refresh failed: %s', result.error?.message ?? 'unknown error')
|
|
105
|
+
return
|
|
106
|
+
}
|
|
107
|
+
log('catalog refreshed with %d providers', result.updatedProviders.length)
|
|
108
|
+
invalidateState()
|
|
109
|
+
})()
|
|
110
|
+
}
|
|
111
|
+
await catalogRefreshTask
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
function hasProviderAuth(state: Record<string, import('./state.js').ProviderState>, providerId: string): boolean {
|
|
115
|
+
const providerState = state[providerId]
|
|
116
|
+
return providerState !== undefined && providerState.source !== 'none'
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
async function checkProviderUsable(providerId: string): Promise<boolean> {
|
|
120
|
+
const catalogProvider = catalog.getProvider(providerId)
|
|
121
|
+
const bundledKey = resolveBundledProviderKey(providerId, catalogProvider)
|
|
122
|
+
if (bundledKey === undefined) return false
|
|
123
|
+
return isProviderInstalled(bundledKey)
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
return {
|
|
127
|
+
async getLanguageModel(providerId: string, modelId: string): Promise<LanguageModelV3> {
|
|
128
|
+
await ensureCatalogEnriched()
|
|
129
|
+
const state = await getState()
|
|
130
|
+
const providerState = state[providerId]
|
|
131
|
+
|
|
132
|
+
log(
|
|
133
|
+
'getLanguageModel(%s, %s) — auth: source=%s, location=%s',
|
|
134
|
+
providerId,
|
|
135
|
+
modelId,
|
|
136
|
+
providerState?.source ?? 'none',
|
|
137
|
+
providerState?.location ?? 'unknown'
|
|
138
|
+
)
|
|
139
|
+
if (providerState === undefined) {
|
|
140
|
+
throw new Error(`Provider not found in catalog: ${providerId}`)
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
const catalogProvider = catalog.getProvider(providerId)
|
|
144
|
+
const bundledKey = resolveBundledProviderKey(providerId, catalogProvider)
|
|
145
|
+
|
|
146
|
+
if (bundledKey === undefined) {
|
|
147
|
+
throw new Error(
|
|
148
|
+
`No bundled provider mapping found for: ${providerId}. Set bundledProvider in catalog extend() config.`
|
|
149
|
+
)
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
const factory = await loadProvider(bundledKey)
|
|
153
|
+
|
|
154
|
+
if (factory === undefined) {
|
|
155
|
+
throw new Error(`Provider package not available: ${bundledKey}. Install it with: npm install ${bundledKey}`)
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
log('creating SDK for %s using %s', providerId, bundledKey)
|
|
159
|
+
|
|
160
|
+
const sdkOptions = { ...providerState.options }
|
|
161
|
+
if (providerState.key !== undefined) {
|
|
162
|
+
sdkOptions.apiKey = providerState.key
|
|
163
|
+
}
|
|
164
|
+
// authToken and apiKey must not coexist (e.g. @ai-sdk/anthropic rejects both)
|
|
165
|
+
if (sdkOptions.authToken !== undefined) {
|
|
166
|
+
sdkOptions.apiKey = undefined
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
const sdk = factory(sdkOptions)
|
|
170
|
+
log('calling sdk.languageModel(%s)', modelId)
|
|
171
|
+
return sdk.languageModel(modelId)
|
|
172
|
+
},
|
|
173
|
+
|
|
174
|
+
extend(extendConfig: ExtendConfig): void {
|
|
175
|
+
catalog.extend(extendConfig)
|
|
176
|
+
invalidateState()
|
|
177
|
+
},
|
|
178
|
+
|
|
179
|
+
async listProviders(options?: ProviderListOptions): Promise<CatalogProvider[]> {
|
|
180
|
+
await ensureCatalogEnriched()
|
|
181
|
+
const allProviders = catalog.listProviders()
|
|
182
|
+
|
|
183
|
+
const usabilityChecks = await Promise.all(
|
|
184
|
+
allProviders.map(async (p) => ({ provider: p, usable: await checkProviderUsable(p.id) }))
|
|
185
|
+
)
|
|
186
|
+
const installedProviders = usabilityChecks.filter((r) => r.usable).map((r) => r.provider)
|
|
187
|
+
|
|
188
|
+
if (options?.includeUnavailable === true) {
|
|
189
|
+
return installedProviders
|
|
190
|
+
}
|
|
191
|
+
const state = await getState()
|
|
192
|
+
return installedProviders.filter((provider) => hasProviderAuth(state, provider.id))
|
|
193
|
+
},
|
|
194
|
+
|
|
195
|
+
async listModels(providerId?: string, options?: ModelListOptions): Promise<ModelDefinition[]> {
|
|
196
|
+
await ensureCatalogEnriched()
|
|
197
|
+
if (options?.includeUnavailable === true) {
|
|
198
|
+
return catalog.listModels(providerId)
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
const state = await getState()
|
|
202
|
+
if (providerId !== undefined) {
|
|
203
|
+
if (!hasProviderAuth(state, providerId)) {
|
|
204
|
+
return []
|
|
205
|
+
}
|
|
206
|
+
return catalog.listModels(providerId)
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
const allProviders = catalog.listProviders()
|
|
210
|
+
const usabilityChecks = await Promise.all(
|
|
211
|
+
allProviders.map(async (p) => ({
|
|
212
|
+
provider: p,
|
|
213
|
+
usable: (await checkProviderUsable(p.id)) && hasProviderAuth(state, p.id),
|
|
214
|
+
}))
|
|
215
|
+
)
|
|
216
|
+
|
|
217
|
+
const results: ModelDefinition[] = []
|
|
218
|
+
for (const { provider, usable } of usabilityChecks) {
|
|
219
|
+
if (usable) {
|
|
220
|
+
results.push(...catalog.listModels(provider.id))
|
|
221
|
+
}
|
|
222
|
+
}
|
|
223
|
+
return results
|
|
224
|
+
},
|
|
225
|
+
|
|
226
|
+
async getModel(
|
|
227
|
+
providerId: string,
|
|
228
|
+
modelId: string,
|
|
229
|
+
options?: GetModelOptions
|
|
230
|
+
): Promise<ModelDefinition | undefined> {
|
|
231
|
+
await ensureCatalogEnriched()
|
|
232
|
+
if (options?.includeUnavailable !== true) {
|
|
233
|
+
const state = await getState()
|
|
234
|
+
if (!hasProviderAuth(state, providerId)) {
|
|
235
|
+
return undefined
|
|
236
|
+
}
|
|
237
|
+
}
|
|
238
|
+
return catalog.getModel(providerId, modelId)
|
|
239
|
+
},
|
|
240
|
+
}
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
export function getLanguageModel(
|
|
244
|
+
providerId: string,
|
|
245
|
+
modelId: string,
|
|
246
|
+
config?: ProviderStoreConfig
|
|
247
|
+
): Promise<LanguageModelV3> {
|
|
248
|
+
return createProviderStore(createAuthStore(), config).getLanguageModel(providerId, modelId)
|
|
249
|
+
}
|
|
@@ -0,0 +1,163 @@
|
|
|
1
|
+
import type { AuthStore } from '../auth/store.js'
|
|
2
|
+
import type { Catalog } from '../catalog/catalog.js'
|
|
3
|
+
import { createLogger } from '../logger.js'
|
|
4
|
+
import { loadPluginOptions } from '../plugin/index.js'
|
|
5
|
+
import type { SecretRef } from '../types/auth.js'
|
|
6
|
+
import type { ProviderUserConfig } from '../types/provider.js'
|
|
7
|
+
|
|
8
|
+
const log = createLogger('provider:state')
|
|
9
|
+
|
|
10
|
+
export interface ProviderState {
|
|
11
|
+
id: string
|
|
12
|
+
key?: string
|
|
13
|
+
options: Record<string, unknown>
|
|
14
|
+
source: 'env' | 'disk' | 'auth' | 'plugin' | 'config' | 'none'
|
|
15
|
+
location?: string
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
async function resolveSecretRef(ref: SecretRef): Promise<string | undefined> {
|
|
19
|
+
if (typeof ref === 'string') return ref
|
|
20
|
+
if (ref.type === 'plain') return ref.value
|
|
21
|
+
if (ref.type === 'env') return process.env[ref.name]
|
|
22
|
+
return undefined
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
const GOOGLE_PROVIDERS = new Set(['@ai-sdk/google', '@ai-sdk/google-vertex'])
|
|
26
|
+
function normalizeProviderBaseURL(providerId: string, baseURL: string): string {
|
|
27
|
+
if (providerId !== 'openai') return baseURL
|
|
28
|
+
|
|
29
|
+
try {
|
|
30
|
+
const parsed = new URL(baseURL)
|
|
31
|
+
const path = parsed.pathname.replace(/\/+$/, '')
|
|
32
|
+
if (path === '' || path === '/') {
|
|
33
|
+
parsed.pathname = '/v1'
|
|
34
|
+
}
|
|
35
|
+
return parsed.toString().replace(/\/$/, '')
|
|
36
|
+
} catch {
|
|
37
|
+
return baseURL
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
function resolveAuthBaseURL(authCred: Record<string, unknown> | undefined): string | undefined {
|
|
42
|
+
if (authCred === undefined) return undefined
|
|
43
|
+
if (typeof authCred.baseURL === 'string' && authCred.baseURL.trim().length > 0) return authCred.baseURL.trim()
|
|
44
|
+
if (typeof authCred.apiHost === 'string' && authCred.apiHost.trim().length > 0) return authCred.apiHost.trim()
|
|
45
|
+
if (typeof authCred.host === 'string' && authCred.host.trim().length > 0) return authCred.host.trim()
|
|
46
|
+
return undefined
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
export async function buildProviderState(config: {
|
|
50
|
+
catalog: Catalog
|
|
51
|
+
authStore: AuthStore
|
|
52
|
+
userConfig?: Record<string, ProviderUserConfig>
|
|
53
|
+
}): Promise<Record<string, ProviderState>> {
|
|
54
|
+
const { catalog, authStore, userConfig } = config
|
|
55
|
+
|
|
56
|
+
log('building provider state')
|
|
57
|
+
|
|
58
|
+
const allProviders = catalog.listProviders()
|
|
59
|
+
const authCredentials = await authStore.all()
|
|
60
|
+
|
|
61
|
+
log('found %d catalog providers, %d auth entries', allProviders.length, Object.keys(authCredentials).length)
|
|
62
|
+
|
|
63
|
+
const result: Record<string, ProviderState> = {}
|
|
64
|
+
|
|
65
|
+
for (const catalogProvider of allProviders) {
|
|
66
|
+
const pid = catalogProvider.id
|
|
67
|
+
const options: Record<string, unknown> = {}
|
|
68
|
+
let key: string | undefined
|
|
69
|
+
let source: ProviderState['source'] = 'none'
|
|
70
|
+
let location: string | undefined
|
|
71
|
+
if (catalogProvider.baseURL !== undefined) {
|
|
72
|
+
options.baseURL = normalizeProviderBaseURL(pid, catalogProvider.baseURL)
|
|
73
|
+
}
|
|
74
|
+
if (catalogProvider.headers !== undefined) options.headers = { ...catalogProvider.headers }
|
|
75
|
+
if (catalogProvider.options !== undefined) Object.assign(options, catalogProvider.options)
|
|
76
|
+
if (catalogProvider.env !== undefined) {
|
|
77
|
+
for (const envVar of catalogProvider.env) {
|
|
78
|
+
const val = process.env[envVar]
|
|
79
|
+
if (val !== undefined) {
|
|
80
|
+
key = val
|
|
81
|
+
source = 'env'
|
|
82
|
+
location = `env:${envVar}`
|
|
83
|
+
break
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
const authCred = authCredentials[pid]
|
|
88
|
+
const authBaseURL = resolveAuthBaseURL(authCred as Record<string, unknown> | undefined)
|
|
89
|
+
if (authBaseURL !== undefined) {
|
|
90
|
+
options.baseURL = normalizeProviderBaseURL(pid, authBaseURL)
|
|
91
|
+
}
|
|
92
|
+
if (authCred?.key !== undefined) {
|
|
93
|
+
key = authCred.key
|
|
94
|
+
source = 'auth'
|
|
95
|
+
location = authCred.location
|
|
96
|
+
const bp = catalogProvider.bundledProvider
|
|
97
|
+
if (authCred.type === 'oauth' && bp !== undefined) {
|
|
98
|
+
if (GOOGLE_PROVIDERS.has(bp)) {
|
|
99
|
+
const token = authCred.key
|
|
100
|
+
options.fetch = (url: string | URL | Request, init?: RequestInit) => {
|
|
101
|
+
const h = new Headers(init?.headers)
|
|
102
|
+
h.delete('x-goog-api-key')
|
|
103
|
+
h.set('Authorization', `Bearer ${token}`)
|
|
104
|
+
return globalThis.fetch(url, { ...init, headers: h })
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
const getAuth = async () => {
|
|
111
|
+
const preferred = await authStore.getPreferred?.(pid, 'oauth')
|
|
112
|
+
return preferred ?? authCred ?? { type: 'api' as const }
|
|
113
|
+
}
|
|
114
|
+
const setAuth = async (credential: Parameters<AuthStore['set']>[1]) => {
|
|
115
|
+
await authStore.set(pid, credential)
|
|
116
|
+
}
|
|
117
|
+
const pluginOpts = await loadPluginOptions(pid, getAuth, { id: pid, name: catalogProvider.name }, setAuth)
|
|
118
|
+
if (pluginOpts !== undefined) {
|
|
119
|
+
Object.assign(options, pluginOpts)
|
|
120
|
+
const pluginKey = pluginOpts.apiKey
|
|
121
|
+
if (typeof pluginKey === 'string') {
|
|
122
|
+
key = pluginKey
|
|
123
|
+
source = 'plugin'
|
|
124
|
+
}
|
|
125
|
+
// Resolve actual auth credential to track correct location
|
|
126
|
+
const resolvedAuth = await getAuth()
|
|
127
|
+
if (resolvedAuth.location) {
|
|
128
|
+
location = resolvedAuth.location
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
const userCfg = userConfig?.[pid]
|
|
133
|
+
if (userCfg !== undefined) {
|
|
134
|
+
if (userCfg.baseURL !== undefined) options.baseURL = normalizeProviderBaseURL(pid, userCfg.baseURL)
|
|
135
|
+
if (userCfg.headers !== undefined) {
|
|
136
|
+
const existingHeaders = options.headers
|
|
137
|
+
options.headers = {
|
|
138
|
+
...(existingHeaders !== null && typeof existingHeaders === 'object' && !Array.isArray(existingHeaders)
|
|
139
|
+
? (existingHeaders as Record<string, string>)
|
|
140
|
+
: {}),
|
|
141
|
+
...userCfg.headers,
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
if (userCfg.options !== undefined) Object.assign(options, userCfg.options)
|
|
145
|
+
if (userCfg.apiKey !== undefined) {
|
|
146
|
+
const resolved = await resolveSecretRef(userCfg.apiKey)
|
|
147
|
+
if (resolved !== undefined) {
|
|
148
|
+
key = resolved
|
|
149
|
+
source = 'config'
|
|
150
|
+
log('%s: resolved key from user config', pid)
|
|
151
|
+
}
|
|
152
|
+
}
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
if (source !== 'none') {
|
|
156
|
+
log('%s: source=%s, location=%s', pid, source, location ?? 'n/a')
|
|
157
|
+
}
|
|
158
|
+
result[pid] = { id: pid, key, options, source, location }
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
log('provider state built for %d providers', Object.keys(result).length)
|
|
162
|
+
return result
|
|
163
|
+
}
|
|
File without changes
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
import { createLogger } from '../logger.js'
|
|
2
|
+
import { MemoryStorage } from './memory.js'
|
|
3
|
+
|
|
4
|
+
const log = createLogger('storage')
|
|
5
|
+
|
|
6
|
+
function defaultNodeDir(): string {
|
|
7
|
+
const g = globalThis as { process?: { env?: Record<string, string | undefined> } }
|
|
8
|
+
const xdg = g.process?.env?.XDG_DATA_HOME
|
|
9
|
+
if (xdg) return `${xdg}/openllmprovider/storage`
|
|
10
|
+
|
|
11
|
+
const home = g.process?.env?.HOME
|
|
12
|
+
if (home) return `${home}/.openllmprovider/storage`
|
|
13
|
+
|
|
14
|
+
return '.openllmprovider/storage'
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
export interface DefaultStorageOptions {
|
|
18
|
+
directory?: string
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
export async function createDefaultStorage(options: DefaultStorageOptions = {}) {
|
|
22
|
+
const directory = options.directory ?? defaultNodeDir()
|
|
23
|
+
try {
|
|
24
|
+
const { FileStorage } = await import('./file.js')
|
|
25
|
+
const storage = new FileStorage({ directory })
|
|
26
|
+
log('using FileStorage at %s', directory)
|
|
27
|
+
return storage
|
|
28
|
+
} catch (err) {
|
|
29
|
+
log('FileStorage unavailable, falling back to MemoryStorage: %o', err)
|
|
30
|
+
return new MemoryStorage()
|
|
31
|
+
}
|
|
32
|
+
}
|
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
import type { StorageAdapter } from './index.js'
|
|
2
|
+
|
|
3
|
+
interface FsLike {
|
|
4
|
+
readFile(path: string, encoding: string): Promise<string>
|
|
5
|
+
writeFile(path: string, data: string, encoding: string): Promise<void>
|
|
6
|
+
mkdir(path: string, options: { recursive: boolean }): Promise<string | undefined>
|
|
7
|
+
unlink(path: string): Promise<void>
|
|
8
|
+
readdir(path: string): Promise<string[]>
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
interface PathLike {
|
|
12
|
+
join(...paths: string[]): string
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
async function getFs(): Promise<FsLike> {
|
|
16
|
+
return (await import('node:fs/promises')) as unknown as FsLike
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
async function getPath(): Promise<PathLike> {
|
|
20
|
+
return await import('node:path')
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
const UNSAFE_CHARS = /[/\\:*?"<>|]/g
|
|
24
|
+
|
|
25
|
+
function sanitizeKey(key: string): string {
|
|
26
|
+
return key.replace(UNSAFE_CHARS, '_')
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
export interface FileStorageOptions {
|
|
30
|
+
directory: string
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
export class FileStorage implements StorageAdapter {
|
|
34
|
+
private readonly directory: string
|
|
35
|
+
|
|
36
|
+
constructor(options: FileStorageOptions) {
|
|
37
|
+
this.directory = options.directory
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
private async filePath(key: string): Promise<string> {
|
|
41
|
+
const path = await getPath()
|
|
42
|
+
return path.join(this.directory, sanitizeKey(key))
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
async get(key: string): Promise<string | null> {
|
|
46
|
+
const fs = await getFs()
|
|
47
|
+
try {
|
|
48
|
+
return await fs.readFile(await this.filePath(key), 'utf-8')
|
|
49
|
+
} catch (err: unknown) {
|
|
50
|
+
if (isEnoent(err)) return null
|
|
51
|
+
throw err
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
async set(key: string, value: string): Promise<void> {
|
|
56
|
+
const fs = await getFs()
|
|
57
|
+
await fs.mkdir(this.directory, { recursive: true })
|
|
58
|
+
await fs.writeFile(await this.filePath(key), value, 'utf-8')
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
async remove(key: string): Promise<void> {
|
|
62
|
+
const fs = await getFs()
|
|
63
|
+
try {
|
|
64
|
+
await fs.unlink(await this.filePath(key))
|
|
65
|
+
} catch (err: unknown) {
|
|
66
|
+
if (isEnoent(err)) return
|
|
67
|
+
throw err
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
async list(): Promise<string[]> {
|
|
72
|
+
const fs = await getFs()
|
|
73
|
+
try {
|
|
74
|
+
return await fs.readdir(this.directory)
|
|
75
|
+
} catch (err: unknown) {
|
|
76
|
+
if (isEnoent(err)) return []
|
|
77
|
+
throw err
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
function isEnoent(err: unknown): boolean {
|
|
83
|
+
return typeof err === 'object' && err !== null && (err as { code?: string }).code === 'ENOENT'
|
|
84
|
+
}
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
export interface StorageAdapter {
|
|
2
|
+
get(key: string): Promise<string | null>
|
|
3
|
+
set(key: string, value: string): Promise<void>
|
|
4
|
+
remove(key: string): Promise<void>
|
|
5
|
+
list(): Promise<string[]>
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
export { MemoryStorage } from './memory.js'
|
|
9
|
+
export { FileStorage, type FileStorageOptions } from './file.js'
|
|
10
|
+
export { createDefaultStorage, type DefaultStorageOptions } from './auto.js'
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
import type { StorageAdapter } from './index.js'
|
|
2
|
+
|
|
3
|
+
export class MemoryStorage implements StorageAdapter {
|
|
4
|
+
private store: Map<string, string> = new Map()
|
|
5
|
+
|
|
6
|
+
get(key: string): Promise<string | null> {
|
|
7
|
+
return Promise.resolve(this.store.get(key) ?? null)
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
set(key: string, value: string): Promise<void> {
|
|
11
|
+
this.store.set(key, value)
|
|
12
|
+
return Promise.resolve()
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
remove(key: string): Promise<void> {
|
|
16
|
+
this.store.delete(key)
|
|
17
|
+
return Promise.resolve()
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
list(): Promise<string[]> {
|
|
21
|
+
return Promise.resolve([...this.store.keys()])
|
|
22
|
+
}
|
|
23
|
+
}
|
|
File without changes
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
import { z } from 'zod'
|
|
2
|
+
|
|
3
|
+
export type SecretRef =
|
|
4
|
+
| { type: 'plain'; value: string }
|
|
5
|
+
| { type: 'env'; name: string }
|
|
6
|
+
| { type: 'storage'; key: string }
|
|
7
|
+
| string
|
|
8
|
+
|
|
9
|
+
export interface SecretResolver {
|
|
10
|
+
resolve(ref: SecretRef): Promise<string>
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
export const SecretRefSchema: z.ZodType<SecretRef> = z.union([
|
|
14
|
+
z.object({ type: z.literal('plain'), value: z.string() }),
|
|
15
|
+
z.object({ type: z.literal('env'), name: z.string() }),
|
|
16
|
+
z.object({ type: z.literal('storage'), key: z.string() }),
|
|
17
|
+
z.string(),
|
|
18
|
+
])
|