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.
Files changed (91) hide show
  1. package/README.md +192 -0
  2. package/dist/auth/index.cjs +6 -0
  3. package/dist/auth/index.d.cts +3 -0
  4. package/dist/auth/index.d.mts +3 -0
  5. package/dist/auth/index.mjs +3 -0
  6. package/dist/auto-C2hXJY13.d.cts +33 -0
  7. package/dist/auto-C2hXJY13.d.cts.map +1 -0
  8. package/dist/auto-CBqNYBXs.mjs +48 -0
  9. package/dist/auto-CBqNYBXs.mjs.map +1 -0
  10. package/dist/auto-CInerwvs.d.mts +33 -0
  11. package/dist/auto-CInerwvs.d.mts.map +1 -0
  12. package/dist/auto-D77wgMqO.cjs +59 -0
  13. package/dist/auto-D77wgMqO.cjs.map +1 -0
  14. package/dist/file-DB-rxfzi.mjs +77 -0
  15. package/dist/file-DB-rxfzi.mjs.map +1 -0
  16. package/dist/file-DZ7FGcSW.cjs +73 -0
  17. package/dist/file-DZ7FGcSW.cjs.map +1 -0
  18. package/dist/index.cjs +1909 -0
  19. package/dist/index.cjs.map +1 -0
  20. package/dist/index.d.cts +1239 -0
  21. package/dist/index.d.cts.map +1 -0
  22. package/dist/index.d.mts +1241 -0
  23. package/dist/index.d.mts.map +1 -0
  24. package/dist/index.mjs +1891 -0
  25. package/dist/index.mjs.map +1 -0
  26. package/dist/logger-BsHpI_fH.mjs +11 -0
  27. package/dist/logger-BsHpI_fH.mjs.map +1 -0
  28. package/dist/logger-jRimlMFR.cjs +69 -0
  29. package/dist/logger-jRimlMFR.cjs.map +1 -0
  30. package/dist/plugin/index.cjs +29 -0
  31. package/dist/plugin/index.cjs.map +1 -0
  32. package/dist/plugin/index.d.cts +10 -0
  33. package/dist/plugin/index.d.cts.map +1 -0
  34. package/dist/plugin/index.d.mts +10 -0
  35. package/dist/plugin/index.d.mts.map +1 -0
  36. package/dist/plugin/index.mjs +25 -0
  37. package/dist/plugin/index.mjs.map +1 -0
  38. package/dist/plugin-BkeUu5LW.d.mts +46 -0
  39. package/dist/plugin-BkeUu5LW.d.mts.map +1 -0
  40. package/dist/plugin-wK7RmJhZ.d.cts +46 -0
  41. package/dist/plugin-wK7RmJhZ.d.cts.map +1 -0
  42. package/dist/resolver-BA7LWSJO.mjs +645 -0
  43. package/dist/resolver-BA7LWSJO.mjs.map +1 -0
  44. package/dist/resolver-BMTvzTt9.cjs +662 -0
  45. package/dist/resolver-BMTvzTt9.cjs.map +1 -0
  46. package/dist/resolver-MgJryMWG.d.cts +75 -0
  47. package/dist/resolver-MgJryMWG.d.cts.map +1 -0
  48. package/dist/resolver-_gfXzr_S.d.mts +76 -0
  49. package/dist/resolver-_gfXzr_S.d.mts.map +1 -0
  50. package/dist/storage/index.cjs +7 -0
  51. package/dist/storage/index.d.cts +12 -0
  52. package/dist/storage/index.d.cts.map +1 -0
  53. package/dist/storage/index.d.mts +12 -0
  54. package/dist/storage/index.d.mts.map +1 -0
  55. package/dist/storage/index.mjs +4 -0
  56. package/package.json +137 -0
  57. package/src/auth/.gitkeep +0 -0
  58. package/src/auth/index.ts +10 -0
  59. package/src/auth/resolver.ts +46 -0
  60. package/src/auth/scanners.ts +462 -0
  61. package/src/auth/store.ts +357 -0
  62. package/src/catalog/.gitkeep +0 -0
  63. package/src/catalog/catalog.ts +302 -0
  64. package/src/catalog/index.ts +17 -0
  65. package/src/catalog/mapper.ts +129 -0
  66. package/src/catalog/merger.ts +99 -0
  67. package/src/index.ts +37 -0
  68. package/src/logger.ts +7 -0
  69. package/src/plugin/.gitkeep +0 -0
  70. package/src/plugin/anthropic.test.ts +505 -0
  71. package/src/plugin/anthropic.ts +324 -0
  72. package/src/plugin/codex.ts +656 -0
  73. package/src/plugin/copilot.ts +161 -0
  74. package/src/plugin/google.ts +454 -0
  75. package/src/plugin/index.ts +30 -0
  76. package/src/provider/.gitkeep +0 -0
  77. package/src/provider/bundled.ts +59 -0
  78. package/src/provider/index.ts +249 -0
  79. package/src/provider/state.ts +163 -0
  80. package/src/storage/.gitkeep +0 -0
  81. package/src/storage/auto.ts +32 -0
  82. package/src/storage/file.ts +84 -0
  83. package/src/storage/index.ts +10 -0
  84. package/src/storage/memory.ts +23 -0
  85. package/src/types/.gitkeep +0 -0
  86. package/src/types/auth.ts +18 -0
  87. package/src/types/errors.ts +87 -0
  88. package/src/types/index.ts +26 -0
  89. package/src/types/model.ts +88 -0
  90. package/src/types/plugin.ts +49 -0
  91. 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
+ ])