@swarmclawai/swarmclaw 1.2.5 → 1.2.6

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@swarmclawai/swarmclaw",
3
- "version": "1.2.5",
3
+ "version": "1.2.6",
4
4
  "description": "Self-hosted AI runtime for OpenClaw, delegation, autonomy, runtime skills, crypto wallets, and chat platform connectors.",
5
5
  "license": "MIT",
6
6
  "publishConfig": {
@@ -327,8 +327,16 @@ export async function POST(req: Request) {
327
327
  return NextResponse.json(result)
328
328
  }
329
329
  default: {
330
- const { loadProviderConfigs } = await import('@/lib/server/storage')
331
- const configs = loadProviderConfigs() as Record<string, { name?: string; baseUrl?: string; isEnabled?: boolean }>
330
+ let configs: Record<string, { name?: string; baseUrl?: string; isEnabled?: boolean }>
331
+ try {
332
+ const storage = await import('@/lib/server/storage')
333
+ configs = storage.loadProviderConfigs() as Record<string, { name?: string; baseUrl?: string; isEnabled?: boolean }>
334
+ } catch {
335
+ return NextResponse.json(
336
+ { ok: false, message: `Failed to load provider configurations while checking ${provider}.` },
337
+ { status: 500 },
338
+ )
339
+ }
332
340
  const custom = configs[provider]
333
341
  if (custom?.baseUrl) {
334
342
  const result = await checkOpenAiCompatible(
@@ -76,3 +76,111 @@ test('builtin provider override records do not surface as custom providers', ()
76
76
 
77
77
  assert.equal(output.openAiCount, 1)
78
78
  })
79
+
80
+ test('custom provider resolution includes defaultEndpoint and optionalApiKey', () => {
81
+ const output = runWithTempDataDir<{
82
+ defaultEndpoint: string | null
83
+ optionalApiKey: boolean | null
84
+ requiresApiKey: boolean | null
85
+ }>(`
86
+ const storageModule = await import('@/lib/server/storage')
87
+ const storage = storageModule.default || storageModule
88
+ storage.saveProviderConfigs({
89
+ 'custom-llama-server': {
90
+ id: 'custom-llama-server',
91
+ name: 'llama-server',
92
+ type: 'custom',
93
+ baseUrl: 'http://127.0.0.1:8080/v1',
94
+ models: ['my-model'],
95
+ requiresApiKey: false,
96
+ credentialId: null,
97
+ isEnabled: true,
98
+ createdAt: 1,
99
+ updatedAt: 1,
100
+ },
101
+ })
102
+
103
+ const providersModule = await import('@/lib/providers/index')
104
+ const providers = providersModule.default || providersModule
105
+ const resolved = providers.getProvider('custom-llama-server')
106
+
107
+ console.log(JSON.stringify({
108
+ defaultEndpoint: resolved?.defaultEndpoint ?? null,
109
+ optionalApiKey: resolved?.optionalApiKey ?? null,
110
+ requiresApiKey: resolved?.requiresApiKey ?? null,
111
+ }))
112
+ `)
113
+
114
+ assert.equal(output.defaultEndpoint, 'http://127.0.0.1:8080/v1')
115
+ assert.equal(output.optionalApiKey, true)
116
+ assert.equal(output.requiresApiKey, false)
117
+ })
118
+
119
+ test('custom provider with uuid-style ID resolves correctly', () => {
120
+ const output = runWithTempDataDir<{
121
+ resolvedName: string | null
122
+ hasHandler: boolean
123
+ }>(`
124
+ const storageModule = await import('@/lib/server/storage')
125
+ const storage = storageModule.default || storageModule
126
+ storage.saveProviderConfigs({
127
+ 'custom-d20b934e': {
128
+ id: 'custom-d20b934e',
129
+ name: 'My llama-server',
130
+ type: 'custom',
131
+ baseUrl: 'http://127.0.0.1:8080/v1',
132
+ models: ['llama-3.1-8b'],
133
+ requiresApiKey: false,
134
+ credentialId: null,
135
+ isEnabled: true,
136
+ createdAt: 1,
137
+ updatedAt: 1,
138
+ },
139
+ })
140
+
141
+ const providersModule = await import('@/lib/providers/index')
142
+ const providers = providersModule.default || providersModule
143
+ const resolved = providers.getProvider('custom-d20b934e')
144
+
145
+ console.log(JSON.stringify({
146
+ resolvedName: resolved?.name ?? null,
147
+ hasHandler: typeof resolved?.handler?.streamChat === 'function',
148
+ }))
149
+ `)
150
+
151
+ assert.equal(output.resolvedName, 'My llama-server')
152
+ assert.equal(output.hasHandler, true)
153
+ })
154
+
155
+ test('disabled custom providers are not resolved by getProvider', () => {
156
+ const output = runWithTempDataDir<{
157
+ resolved: boolean
158
+ }>(`
159
+ const storageModule = await import('@/lib/server/storage')
160
+ const storage = storageModule.default || storageModule
161
+ storage.saveProviderConfigs({
162
+ 'custom-disabled': {
163
+ id: 'custom-disabled',
164
+ name: 'Disabled Provider',
165
+ type: 'custom',
166
+ baseUrl: 'http://127.0.0.1:8080/v1',
167
+ models: ['test'],
168
+ requiresApiKey: false,
169
+ credentialId: null,
170
+ isEnabled: false,
171
+ createdAt: 1,
172
+ updatedAt: 1,
173
+ },
174
+ })
175
+
176
+ const providersModule = await import('@/lib/providers/index')
177
+ const providers = providersModule.default || providersModule
178
+ const resolved = providers.getProvider('custom-disabled')
179
+
180
+ console.log(JSON.stringify({
181
+ resolved: resolved !== null,
182
+ }))
183
+ `)
184
+
185
+ assert.equal(output.resolved, false)
186
+ })
@@ -286,7 +286,8 @@ function getCustomProviders(): Record<string, CustomProviderConfig> {
286
286
  return Object.fromEntries(
287
287
  Object.entries(configs).filter(([, config]) => config?.type === 'custom'),
288
288
  )
289
- } catch {
289
+ } catch (err) {
290
+ log.warn(TAG, 'Failed to load custom providers from storage', errorMessage(err))
290
291
  return {}
291
292
  }
292
293
  }
@@ -325,6 +326,7 @@ export function getProviderList(): ProviderInfo[] {
325
326
  defaultModels: c.models,
326
327
  supportsModelDiscovery: false,
327
328
  requiresApiKey: c.requiresApiKey,
329
+ optionalApiKey: !c.requiresApiKey,
328
330
  requiresEndpoint: false as boolean,
329
331
  defaultEndpoint: c.baseUrl,
330
332
  }))
@@ -348,26 +350,47 @@ export function getProviderList(): ProviderInfo[] {
348
350
  return [...builtins, ...customs, ...extensionProviders]
349
351
  }
350
352
 
353
+ function buildCustomProviderConfig(custom: CustomProviderConfig): BuiltinProviderConfig {
354
+ return {
355
+ id: custom.id as ProviderId,
356
+ name: custom.name,
357
+ models: custom.models,
358
+ requiresApiKey: custom.requiresApiKey,
359
+ optionalApiKey: !custom.requiresApiKey,
360
+ requiresEndpoint: false,
361
+ defaultEndpoint: custom.baseUrl,
362
+ handler: {
363
+ streamChat: async (opts) => {
364
+ const patchedSession = { ...opts.session, apiEndpoint: custom.baseUrl }
365
+ const { streamOpenAiChat } = await import('./openai')
366
+ return streamOpenAiChat({ ...opts, session: patchedSession })
367
+ },
368
+ },
369
+ }
370
+ }
371
+
351
372
  export function getProvider(id: string): BuiltinProviderConfig | null {
352
373
  if (PROVIDERS[id]) return PROVIDERS[id]
353
-
374
+
354
375
  // Check custom providers
355
376
  const customs = getCustomProviders()
356
377
  const custom = customs[id]
357
378
  if (custom?.isEnabled) {
358
- return {
359
- id: custom.id as ProviderId,
360
- name: custom.name,
361
- models: custom.models,
362
- requiresApiKey: custom.requiresApiKey,
363
- requiresEndpoint: false,
364
- handler: {
365
- streamChat: async (opts) => {
366
- const patchedSession = { ...opts.session, apiEndpoint: custom.baseUrl }
367
- const { streamOpenAiChat } = await import('./openai')
368
- return streamOpenAiChat({ ...opts, session: patchedSession })
369
- },
370
- },
379
+ return buildCustomProviderConfig(custom)
380
+ }
381
+
382
+ // Fallback: direct single-item DB lookup for custom-* providers
383
+ if (id.startsWith('custom-') && !custom) {
384
+ try {
385
+ // eslint-disable-next-line @typescript-eslint/no-require-imports
386
+ const { loadStoredItem } = require('@/lib/server/storage') as typeof import('@/lib/server/storage')
387
+ const directConfig = loadStoredItem('provider_configs', id) as CustomProviderConfig | null
388
+ if (directConfig?.type === 'custom' && directConfig.isEnabled) {
389
+ log.info(TAG, `Resolved custom provider '${id}' via direct DB lookup (batch load missed it)`)
390
+ return buildCustomProviderConfig(directConfig)
391
+ }
392
+ } catch (err) {
393
+ log.warn(TAG, `Direct DB lookup failed for provider '${id}'`, errorMessage(err))
371
394
  }
372
395
  }
373
396