@actuate-media/cms-core 0.42.0 → 0.44.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 (72) hide show
  1. package/dist/__tests__/ai/ai.test.d.ts +2 -0
  2. package/dist/__tests__/ai/ai.test.d.ts.map +1 -0
  3. package/dist/__tests__/ai/ai.test.js +261 -0
  4. package/dist/__tests__/ai/ai.test.js.map +1 -0
  5. package/dist/__tests__/ai/resolve.test.d.ts +2 -0
  6. package/dist/__tests__/ai/resolve.test.d.ts.map +1 -0
  7. package/dist/__tests__/ai/resolve.test.js +164 -0
  8. package/dist/__tests__/ai/resolve.test.js.map +1 -0
  9. package/dist/ai/catalog.d.ts +29 -0
  10. package/dist/ai/catalog.d.ts.map +1 -0
  11. package/dist/ai/catalog.js +340 -0
  12. package/dist/ai/catalog.js.map +1 -0
  13. package/dist/ai/config-store.d.ts +55 -0
  14. package/dist/ai/config-store.d.ts.map +1 -0
  15. package/dist/ai/config-store.js +177 -0
  16. package/dist/ai/config-store.js.map +1 -0
  17. package/dist/ai/feature-defaults.d.ts +20 -0
  18. package/dist/ai/feature-defaults.d.ts.map +1 -0
  19. package/dist/ai/feature-defaults.js +104 -0
  20. package/dist/ai/feature-defaults.js.map +1 -0
  21. package/dist/ai/index.d.ts +14 -0
  22. package/dist/ai/index.d.ts.map +1 -0
  23. package/dist/ai/index.js +14 -0
  24. package/dist/ai/index.js.map +1 -0
  25. package/dist/ai/models.d.ts +37 -0
  26. package/dist/ai/models.d.ts.map +1 -0
  27. package/dist/ai/models.js +126 -0
  28. package/dist/ai/models.js.map +1 -0
  29. package/dist/ai/providers/anthropic.d.ts +9 -0
  30. package/dist/ai/providers/anthropic.d.ts.map +1 -0
  31. package/dist/ai/providers/anthropic.js +36 -0
  32. package/dist/ai/providers/anthropic.js.map +1 -0
  33. package/dist/ai/providers/custom.d.ts +11 -0
  34. package/dist/ai/providers/custom.d.ts.map +1 -0
  35. package/dist/ai/providers/custom.js +70 -0
  36. package/dist/ai/providers/custom.js.map +1 -0
  37. package/dist/ai/providers/http.d.ts +32 -0
  38. package/dist/ai/providers/http.d.ts.map +1 -0
  39. package/dist/ai/providers/http.js +90 -0
  40. package/dist/ai/providers/http.js.map +1 -0
  41. package/dist/ai/providers/index.d.ts +16 -0
  42. package/dist/ai/providers/index.d.ts.map +1 -0
  43. package/dist/ai/providers/index.js +25 -0
  44. package/dist/ai/providers/index.js.map +1 -0
  45. package/dist/ai/providers/openai.d.ts +10 -0
  46. package/dist/ai/providers/openai.d.ts.map +1 -0
  47. package/dist/ai/providers/openai.js +41 -0
  48. package/dist/ai/providers/openai.js.map +1 -0
  49. package/dist/ai/providers/xai.d.ts +9 -0
  50. package/dist/ai/providers/xai.d.ts.map +1 -0
  51. package/dist/ai/providers/xai.js +35 -0
  52. package/dist/ai/providers/xai.js.map +1 -0
  53. package/dist/ai/resolve.d.ts +41 -0
  54. package/dist/ai/resolve.d.ts.map +1 -0
  55. package/dist/ai/resolve.js +142 -0
  56. package/dist/ai/resolve.js.map +1 -0
  57. package/dist/ai/types.d.ts +161 -0
  58. package/dist/ai/types.d.ts.map +1 -0
  59. package/dist/ai/types.js +11 -0
  60. package/dist/ai/types.js.map +1 -0
  61. package/dist/ai/usage.d.ts +18 -0
  62. package/dist/ai/usage.d.ts.map +1 -0
  63. package/dist/ai/usage.js +70 -0
  64. package/dist/ai/usage.js.map +1 -0
  65. package/dist/api/handlers.d.ts.map +1 -1
  66. package/dist/api/handlers.js +523 -0
  67. package/dist/api/handlers.js.map +1 -1
  68. package/dist/index.d.ts +3 -0
  69. package/dist/index.d.ts.map +1 -1
  70. package/dist/index.js +4 -0
  71. package/dist/index.js.map +1 -1
  72. package/package.json +1 -1
@@ -10073,6 +10073,529 @@ export function registerCMSRoutes(router) {
10073
10073
  return internalError(err, 'ai status');
10074
10074
  }
10075
10075
  });
10076
+ // ---------------------------------------------------------------------------
10077
+ // Settings > AI — provider connections, dynamic model catalog, feature flags.
10078
+ // Backed by the `__ai_config__` config document (see `../ai/config-store.ts`).
10079
+ // ADMIN-gated. Provider API keys are encrypted at rest and NEVER returned.
10080
+ // ---------------------------------------------------------------------------
10081
+ const AI_PROVIDER_KEYS = ['openai', 'anthropic', 'xai', 'custom'];
10082
+ function isAiProviderKey(value) {
10083
+ return typeof value === 'string' && AI_PROVIDER_KEYS.includes(value);
10084
+ }
10085
+ function defaultProviderName(provider) {
10086
+ switch (provider) {
10087
+ case 'openai':
10088
+ return 'OpenAI';
10089
+ case 'anthropic':
10090
+ return 'Anthropic';
10091
+ case 'xai':
10092
+ return 'xAI (Grok)';
10093
+ default:
10094
+ return 'Custom provider';
10095
+ }
10096
+ }
10097
+ router.get('/ai/settings', async (request) => {
10098
+ try {
10099
+ const auth = await requireAuth(request);
10100
+ if (auth.error)
10101
+ return auth.error;
10102
+ const adminErr = requireAdminScope(auth.session);
10103
+ if (adminErr)
10104
+ return adminErr;
10105
+ const { getAIConfig, toPublicConfig, getModels, resolveModelOrUnavailable } = await import('../ai/index.js');
10106
+ const config = await getAIConfig(db());
10107
+ const pub = toPublicConfig(config);
10108
+ const catalog = await getModels(db());
10109
+ const defaultModel = resolveModelOrUnavailable(config.settings.defaultModelId, catalog);
10110
+ return json({
10111
+ data: {
10112
+ settings: pub.settings,
10113
+ providers: pub.providers,
10114
+ defaultModel,
10115
+ updatedAt: config.updatedAt ?? null,
10116
+ updatedBy: config.updatedBy ?? null,
10117
+ },
10118
+ });
10119
+ }
10120
+ catch (err) {
10121
+ return internalError(err, 'ai/settings GET');
10122
+ }
10123
+ });
10124
+ router.put('/ai/settings', async (request) => {
10125
+ try {
10126
+ const auth = await requireAuth(request);
10127
+ if (auth.error)
10128
+ return auth.error;
10129
+ const adminErr = requireAdminScope(auth.session);
10130
+ if (adminErr)
10131
+ return adminErr;
10132
+ const body = (await request.json().catch(() => null));
10133
+ if (!body || typeof body !== 'object') {
10134
+ return errorResponse('Request body must be an object', 400);
10135
+ }
10136
+ const { getAIConfig, putAIConfig, getModels } = await import('../ai/index.js');
10137
+ const config = await getAIConfig(db());
10138
+ const catalog = await getModels(db());
10139
+ // Validate the selected default provider exists.
10140
+ if (body.defaultProviderId &&
10141
+ !config.providers.some((p) => p.id === body.defaultProviderId)) {
10142
+ return errorResponse('Selected provider is not connected', 400);
10143
+ }
10144
+ // Validate the selected default model is in the catalog and available.
10145
+ if (body.defaultModelId) {
10146
+ const model = catalog.find((m) => m.id === body.defaultModelId || m.providerModelId === body.defaultModelId);
10147
+ if (!model)
10148
+ return errorResponse('Selected model is not in the catalog', 400);
10149
+ if (model.status === 'unavailable') {
10150
+ return errorResponse('Selected model is currently unavailable', 400);
10151
+ }
10152
+ }
10153
+ // Validate budget.
10154
+ let monthlyTokenLimit = config.settings.budget.monthlyTokenLimit;
10155
+ if (body.budget && 'monthlyTokenLimit' in body.budget) {
10156
+ const v = body.budget.monthlyTokenLimit;
10157
+ if (v === null)
10158
+ monthlyTokenLimit = undefined;
10159
+ else if (typeof v === 'number' && Number.isFinite(v) && v >= 0)
10160
+ monthlyTokenLimit = v;
10161
+ else
10162
+ return errorResponse('monthlyTokenLimit must be a non-negative number or null', 400);
10163
+ }
10164
+ // Validate per-feature model/provider routing overrides. Empty string and
10165
+ // null both clear the override (revert to the default model).
10166
+ const featurePatches = body.features ?? [];
10167
+ for (const patch of featurePatches) {
10168
+ if (patch.modelId != null && patch.modelId !== '') {
10169
+ const m = catalog.find((x) => x.id === patch.modelId || x.providerModelId === patch.modelId);
10170
+ if (!m)
10171
+ return errorResponse(`Model "${patch.modelId}" is not in the catalog`, 400);
10172
+ if (m.status === 'unavailable') {
10173
+ return errorResponse(`Model "${patch.modelId}" is currently unavailable`, 400);
10174
+ }
10175
+ }
10176
+ if (patch.providerId != null &&
10177
+ patch.providerId !== '' &&
10178
+ !config.providers.some((p) => p.id === patch.providerId)) {
10179
+ return errorResponse(`Provider "${patch.providerId}" is not connected`, 400);
10180
+ }
10181
+ }
10182
+ // Apply feature patches by key (ignore unknown keys). Each patch may toggle
10183
+ // `enabled` and/or set the per-feature model/provider routing override.
10184
+ const patchByKey = new Map(featurePatches.map((f) => [f.key, f]));
10185
+ const clearOverride = (v) => v == null || v === '' ? undefined : v;
10186
+ const nextFeatures = config.settings.features.map((f) => {
10187
+ const patch = patchByKey.get(f.key);
10188
+ if (!patch)
10189
+ return f;
10190
+ const next = { ...f };
10191
+ if (typeof patch.enabled === 'boolean')
10192
+ next.enabled = patch.enabled;
10193
+ if ('modelId' in patch)
10194
+ next.modelId = clearOverride(patch.modelId);
10195
+ if ('providerId' in patch)
10196
+ next.providerId = clearOverride(patch.providerId);
10197
+ return next;
10198
+ });
10199
+ const changedFeatures = config.settings.features
10200
+ .map((f) => {
10201
+ const patch = patchByKey.get(f.key);
10202
+ if (!patch)
10203
+ return null;
10204
+ const enabledChanged = typeof patch.enabled === 'boolean' && patch.enabled !== f.enabled;
10205
+ const nextModelId = 'modelId' in patch ? clearOverride(patch.modelId) : f.modelId;
10206
+ const modelChanged = 'modelId' in patch && (nextModelId ?? null) !== (f.modelId ?? null);
10207
+ if (!enabledChanged && !modelChanged)
10208
+ return null;
10209
+ return {
10210
+ key: f.key,
10211
+ ...(enabledChanged ? { enabledFrom: f.enabled, enabledTo: patch.enabled } : {}),
10212
+ ...(modelChanged ? { modelFrom: f.modelId ?? null, modelTo: nextModelId ?? null } : {}),
10213
+ };
10214
+ })
10215
+ .filter((x) => x !== null);
10216
+ const nextSettings = {
10217
+ ...config.settings,
10218
+ defaultProviderId: body.defaultProviderId === null
10219
+ ? undefined
10220
+ : (body.defaultProviderId ?? config.settings.defaultProviderId),
10221
+ defaultModelId: body.defaultModelId === null
10222
+ ? undefined
10223
+ : (body.defaultModelId ?? config.settings.defaultModelId),
10224
+ features: nextFeatures,
10225
+ budget: { ...config.settings.budget, monthlyTokenLimit },
10226
+ };
10227
+ const saved = await putAIConfig(db(), { ...config, settings: nextSettings }, auth.session.userId);
10228
+ try {
10229
+ await logEvent({
10230
+ event: 'update_ai_settings',
10231
+ userId: auth.session.userId,
10232
+ details: {
10233
+ defaultProviderId: nextSettings.defaultProviderId ?? null,
10234
+ defaultModelId: nextSettings.defaultModelId ?? null,
10235
+ monthlyTokenLimit: monthlyTokenLimit ?? null,
10236
+ changedFeatures,
10237
+ source: 'settings.ai',
10238
+ },
10239
+ });
10240
+ }
10241
+ catch {
10242
+ // Audit must never block the write.
10243
+ }
10244
+ const { toPublicConfig } = await import('../ai/index.js');
10245
+ return json({ data: toPublicConfig(saved) });
10246
+ }
10247
+ catch (err) {
10248
+ return internalError(err, 'ai/settings PUT');
10249
+ }
10250
+ });
10251
+ router.post('/ai/providers', async (request) => {
10252
+ try {
10253
+ const auth = await requireAuth(request);
10254
+ if (auth.error)
10255
+ return auth.error;
10256
+ const adminErr = requireAdminScope(auth.session);
10257
+ if (adminErr)
10258
+ return adminErr;
10259
+ const body = (await request.json().catch(() => null));
10260
+ if (!body || !isAiProviderKey(body.provider)) {
10261
+ return errorResponse('A valid `provider` is required', 400);
10262
+ }
10263
+ const provider = body.provider;
10264
+ if (provider === 'custom') {
10265
+ if (!body.baseUrl || typeof body.baseUrl !== 'string') {
10266
+ return errorResponse('A `baseUrl` is required for a custom provider', 400);
10267
+ }
10268
+ const { validateWebhookUrl } = await import('../security/webhook.js');
10269
+ const check = validateWebhookUrl(body.baseUrl);
10270
+ if (!check.valid)
10271
+ return errorResponse(`Invalid baseUrl: ${check.error ?? 'unsafe'}`, 400);
10272
+ }
10273
+ const { getAIConfig, putAIConfig, encryptProviderKey, getEnvApiKey, resolveProviderKey, toPublicProvider, } = await import('../ai/index.js');
10274
+ const { testConnection } = await import('../ai/providers/index.js');
10275
+ const config = await getAIConfig(db());
10276
+ const now = new Date().toISOString();
10277
+ const envManaged = Boolean(getEnvApiKey(provider));
10278
+ let apiKeyEncrypted;
10279
+ let apiKeyLast4;
10280
+ if (!envManaged && typeof body.apiKey === 'string' && body.apiKey.trim()) {
10281
+ const enc = await encryptProviderKey(body.apiKey.trim());
10282
+ apiKeyEncrypted = enc.apiKeyEncrypted;
10283
+ apiKeyLast4 = enc.apiKeyLast4;
10284
+ }
10285
+ const connection = {
10286
+ id: crypto.randomUUID(),
10287
+ provider,
10288
+ displayName: (typeof body.displayName === 'string' && body.displayName.trim()) ||
10289
+ defaultProviderName(provider),
10290
+ enabled: body.enabled !== false,
10291
+ status: 'unknown',
10292
+ apiKeyEncrypted,
10293
+ apiKeyLast4,
10294
+ baseUrl: provider === 'custom' ? body.baseUrl : undefined,
10295
+ organizationId: body.organizationId?.trim() || undefined,
10296
+ projectId: body.projectId?.trim() || undefined,
10297
+ lastTestedAt: undefined,
10298
+ createdAt: now,
10299
+ updatedAt: now,
10300
+ updatedBy: auth.session.userId,
10301
+ };
10302
+ // Optional test-before-save.
10303
+ if (body.test) {
10304
+ const key = await resolveProviderKey(connection);
10305
+ const result = await testConnection(provider, {
10306
+ apiKey: key ?? '',
10307
+ baseUrl: connection.baseUrl,
10308
+ organizationId: connection.organizationId,
10309
+ projectId: connection.projectId,
10310
+ });
10311
+ connection.status = result.status;
10312
+ connection.lastTestedAt = result.testedAt;
10313
+ }
10314
+ const saved = await putAIConfig(db(), { ...config, providers: [...config.providers, connection] }, auth.session.userId);
10315
+ try {
10316
+ await logEvent({
10317
+ event: 'create_ai_provider',
10318
+ userId: auth.session.userId,
10319
+ details: {
10320
+ providerId: connection.id,
10321
+ provider,
10322
+ envManaged,
10323
+ tested: Boolean(body.test),
10324
+ status: connection.status,
10325
+ source: 'settings.ai',
10326
+ },
10327
+ });
10328
+ }
10329
+ catch {
10330
+ /* never block on audit */
10331
+ }
10332
+ const created = saved.providers.find((p) => p.id === connection.id);
10333
+ return json({ data: toPublicProvider(created) }, 201);
10334
+ }
10335
+ catch (err) {
10336
+ return internalError(err, 'ai/providers POST');
10337
+ }
10338
+ });
10339
+ router.put('/ai/providers/:id', async (request, params) => {
10340
+ try {
10341
+ const auth = await requireAuth(request);
10342
+ if (auth.error)
10343
+ return auth.error;
10344
+ const adminErr = requireAdminScope(auth.session);
10345
+ if (adminErr)
10346
+ return adminErr;
10347
+ const body = (await request.json().catch(() => null));
10348
+ if (!body || typeof body !== 'object') {
10349
+ return errorResponse('Request body must be an object', 400);
10350
+ }
10351
+ const { getAIConfig, putAIConfig, encryptProviderKey, getEnvApiKey, toPublicProvider } = await import('../ai/index.js');
10352
+ const config = await getAIConfig(db());
10353
+ const idx = config.providers.findIndex((p) => p.id === params.id);
10354
+ if (idx === -1)
10355
+ return errorResponse('Provider connection not found', 404);
10356
+ const prev = config.providers[idx];
10357
+ if (prev.provider === 'custom' && typeof body.baseUrl === 'string') {
10358
+ const { validateWebhookUrl } = await import('../security/webhook.js');
10359
+ const check = validateWebhookUrl(body.baseUrl);
10360
+ if (!check.valid)
10361
+ return errorResponse(`Invalid baseUrl: ${check.error ?? 'unsafe'}`, 400);
10362
+ }
10363
+ const envManaged = Boolean(getEnvApiKey(prev.provider));
10364
+ let { apiKeyEncrypted, apiKeyLast4 } = prev;
10365
+ let keyChanged = false;
10366
+ if (!envManaged && body.apiKey !== undefined) {
10367
+ if (body.apiKey === null || body.apiKey === '') {
10368
+ apiKeyEncrypted = undefined;
10369
+ apiKeyLast4 = undefined;
10370
+ keyChanged = true;
10371
+ }
10372
+ else if (typeof body.apiKey === 'string' && body.apiKey.trim()) {
10373
+ const enc = await encryptProviderKey(body.apiKey.trim());
10374
+ apiKeyEncrypted = enc.apiKeyEncrypted;
10375
+ apiKeyLast4 = enc.apiKeyLast4;
10376
+ keyChanged = true;
10377
+ }
10378
+ }
10379
+ const updated = {
10380
+ ...prev,
10381
+ displayName: typeof body.displayName === 'string' && body.displayName.trim()
10382
+ ? body.displayName.trim()
10383
+ : prev.displayName,
10384
+ enabled: typeof body.enabled === 'boolean' ? body.enabled : prev.enabled,
10385
+ baseUrl: prev.provider === 'custom' && typeof body.baseUrl === 'string'
10386
+ ? body.baseUrl
10387
+ : prev.baseUrl,
10388
+ organizationId: body.organizationId === null
10389
+ ? undefined
10390
+ : (body.organizationId?.trim() ?? prev.organizationId),
10391
+ projectId: body.projectId === null ? undefined : (body.projectId?.trim() ?? prev.projectId),
10392
+ apiKeyEncrypted,
10393
+ apiKeyLast4,
10394
+ // A new key invalidates the previous test result.
10395
+ status: keyChanged ? 'unknown' : prev.status,
10396
+ lastTestedAt: keyChanged ? undefined : prev.lastTestedAt,
10397
+ updatedAt: new Date().toISOString(),
10398
+ updatedBy: auth.session.userId,
10399
+ };
10400
+ const nextProviders = [...config.providers];
10401
+ nextProviders[idx] = updated;
10402
+ const saved = await putAIConfig(db(), { ...config, providers: nextProviders }, auth.session.userId);
10403
+ try {
10404
+ await logEvent({
10405
+ event: 'update_ai_provider',
10406
+ userId: auth.session.userId,
10407
+ details: {
10408
+ providerId: prev.id,
10409
+ provider: prev.provider,
10410
+ keyChanged,
10411
+ source: 'settings.ai',
10412
+ },
10413
+ });
10414
+ }
10415
+ catch {
10416
+ /* never block on audit */
10417
+ }
10418
+ const out = saved.providers.find((p) => p.id === prev.id);
10419
+ return json({ data: toPublicProvider(out) });
10420
+ }
10421
+ catch (err) {
10422
+ return internalError(err, 'ai/providers PUT');
10423
+ }
10424
+ });
10425
+ router.delete('/ai/providers/:id', async (request, params) => {
10426
+ try {
10427
+ const auth = await requireAuth(request);
10428
+ if (auth.error)
10429
+ return auth.error;
10430
+ const adminErr = requireAdminScope(auth.session);
10431
+ if (adminErr)
10432
+ return adminErr;
10433
+ const id = params.id;
10434
+ if (!id)
10435
+ return errorResponse('Provider connection not found', 404);
10436
+ const { getAIConfig, putAIConfig } = await import('../ai/index.js');
10437
+ const config = await getAIConfig(db());
10438
+ const exists = config.providers.some((p) => p.id === id);
10439
+ if (!exists)
10440
+ return errorResponse('Provider connection not found', 404);
10441
+ const nextCatalog = { ...(config.catalog ?? {}) };
10442
+ delete nextCatalog[id];
10443
+ // Clear default selectors that pointed at the removed provider.
10444
+ const clearedProvider = config.settings.defaultProviderId === id;
10445
+ const nextSettings = clearedProvider
10446
+ ? { ...config.settings, defaultProviderId: undefined, defaultModelId: undefined }
10447
+ : config.settings;
10448
+ await putAIConfig(db(), {
10449
+ ...config,
10450
+ providers: config.providers.filter((p) => p.id !== id),
10451
+ catalog: nextCatalog,
10452
+ settings: nextSettings,
10453
+ }, auth.session.userId);
10454
+ try {
10455
+ await logEvent({
10456
+ event: 'delete_ai_provider',
10457
+ userId: auth.session.userId,
10458
+ details: { providerId: id, source: 'settings.ai' },
10459
+ });
10460
+ }
10461
+ catch {
10462
+ /* never block on audit */
10463
+ }
10464
+ return json({ data: { id: params.id, deleted: true } });
10465
+ }
10466
+ catch (err) {
10467
+ return internalError(err, 'ai/providers DELETE');
10468
+ }
10469
+ });
10470
+ router.post('/ai/providers/:id/test', async (request, params) => {
10471
+ try {
10472
+ const auth = await requireAuth(request);
10473
+ if (auth.error)
10474
+ return auth.error;
10475
+ const adminErr = requireAdminScope(auth.session);
10476
+ if (adminErr)
10477
+ return adminErr;
10478
+ const { getAIConfig, putAIConfig, resolveProviderKey, toPublicProvider } = await import('../ai/index.js');
10479
+ const { testConnection } = await import('../ai/providers/index.js');
10480
+ const config = await getAIConfig(db());
10481
+ const idx = config.providers.findIndex((p) => p.id === params.id);
10482
+ if (idx === -1)
10483
+ return errorResponse('Provider connection not found', 404);
10484
+ const conn = config.providers[idx];
10485
+ const key = await resolveProviderKey(conn);
10486
+ const result = await testConnection(conn.provider, {
10487
+ apiKey: key ?? '',
10488
+ baseUrl: conn.baseUrl,
10489
+ organizationId: conn.organizationId,
10490
+ projectId: conn.projectId,
10491
+ });
10492
+ const updated = { ...conn, status: result.status, lastTestedAt: result.testedAt };
10493
+ const nextProviders = [...config.providers];
10494
+ nextProviders[idx] = updated;
10495
+ const saved = await putAIConfig(db(), { ...config, providers: nextProviders }, auth.session.userId);
10496
+ try {
10497
+ await logEvent({
10498
+ event: 'test_ai_provider',
10499
+ userId: auth.session.userId,
10500
+ details: {
10501
+ providerId: conn.id,
10502
+ provider: conn.provider,
10503
+ status: result.status,
10504
+ source: 'settings.ai',
10505
+ },
10506
+ });
10507
+ }
10508
+ catch {
10509
+ /* never block on audit */
10510
+ }
10511
+ const out = saved.providers.find((p) => p.id === conn.id);
10512
+ return json({ data: { provider: toPublicProvider(out), result } });
10513
+ }
10514
+ catch (err) {
10515
+ return internalError(err, 'ai/providers test');
10516
+ }
10517
+ });
10518
+ router.post('/ai/providers/:id/sync-models', async (request, params) => {
10519
+ try {
10520
+ const auth = await requireAuth(request);
10521
+ if (auth.error)
10522
+ return auth.error;
10523
+ const adminErr = requireAdminScope(auth.session);
10524
+ if (adminErr)
10525
+ return adminErr;
10526
+ const id = params.id;
10527
+ if (!id)
10528
+ return errorResponse('Provider connection not found', 404);
10529
+ const { syncModels } = await import('../ai/index.js');
10530
+ const result = await syncModels(db(), id).catch((err) => {
10531
+ if (err instanceof Error && /not found/i.test(err.message))
10532
+ return null;
10533
+ throw err;
10534
+ });
10535
+ if (!result)
10536
+ return errorResponse('Provider connection not found', 404);
10537
+ try {
10538
+ await logEvent({
10539
+ event: 'sync_ai_models',
10540
+ userId: auth.session.userId,
10541
+ details: {
10542
+ providerId: id,
10543
+ modelCount: result.models.length,
10544
+ fromFallback: result.fromFallback,
10545
+ source: 'settings.ai',
10546
+ },
10547
+ });
10548
+ }
10549
+ catch {
10550
+ /* never block on audit */
10551
+ }
10552
+ return json({
10553
+ data: {
10554
+ models: result.models,
10555
+ fromFallback: result.fromFallback,
10556
+ warning: result.warning ?? null,
10557
+ },
10558
+ });
10559
+ }
10560
+ catch (err) {
10561
+ return internalError(err, 'ai/providers sync-models');
10562
+ }
10563
+ });
10564
+ router.get('/ai/models', async (request) => {
10565
+ try {
10566
+ const auth = await requireAuth(request);
10567
+ if (auth.error)
10568
+ return auth.error;
10569
+ const adminErr = requireAdminScope(auth.session);
10570
+ if (adminErr)
10571
+ return adminErr;
10572
+ const url = new URL(request.url);
10573
+ const connectionId = url.searchParams.get('connectionId') ?? undefined;
10574
+ const provider = url.searchParams.get('provider') ?? undefined;
10575
+ const { getModels } = await import('../ai/index.js');
10576
+ const models = await getModels(db(), { connectionId, provider });
10577
+ return json({ data: { models } });
10578
+ }
10579
+ catch (err) {
10580
+ return internalError(err, 'ai/models GET');
10581
+ }
10582
+ });
10583
+ router.get('/ai/usage/summary', async (request) => {
10584
+ try {
10585
+ const auth = await requireAuth(request);
10586
+ if (auth.error)
10587
+ return auth.error;
10588
+ const adminErr = requireAdminScope(auth.session);
10589
+ if (adminErr)
10590
+ return adminErr;
10591
+ const { getUsageSummary } = await import('../ai/index.js');
10592
+ const summary = await getUsageSummary(db());
10593
+ return json({ data: summary });
10594
+ }
10595
+ catch (err) {
10596
+ return internalError(err, 'ai/usage/summary GET');
10597
+ }
10598
+ });
10076
10599
  router.put('/security', async (request) => {
10077
10600
  try {
10078
10601
  const auth = await requireAuth(request);