@chimerai/cli 0.2.73

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 (129) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +293 -0
  3. package/dist/cli.d.ts +7 -0
  4. package/dist/cli.d.ts.map +1 -0
  5. package/dist/cli.js +317 -0
  6. package/dist/commands/add.d.ts +11 -0
  7. package/dist/commands/add.d.ts.map +1 -0
  8. package/dist/commands/add.js +2126 -0
  9. package/dist/commands/create.d.ts +12 -0
  10. package/dist/commands/create.d.ts.map +1 -0
  11. package/dist/commands/create.js +1703 -0
  12. package/dist/commands/deploy.d.ts +11 -0
  13. package/dist/commands/deploy.d.ts.map +1 -0
  14. package/dist/commands/deploy.js +219 -0
  15. package/dist/commands/dev.d.ts +17 -0
  16. package/dist/commands/dev.d.ts.map +1 -0
  17. package/dist/commands/dev.js +206 -0
  18. package/dist/commands/doctor.d.ts +11 -0
  19. package/dist/commands/doctor.d.ts.map +1 -0
  20. package/dist/commands/doctor.js +728 -0
  21. package/dist/commands/generate.d.ts +19 -0
  22. package/dist/commands/generate.d.ts.map +1 -0
  23. package/dist/commands/generate.js +429 -0
  24. package/dist/commands/init.d.ts +11 -0
  25. package/dist/commands/init.d.ts.map +1 -0
  26. package/dist/commands/init.js +269 -0
  27. package/dist/commands/list.d.ts +12 -0
  28. package/dist/commands/list.d.ts.map +1 -0
  29. package/dist/commands/list.js +328 -0
  30. package/dist/commands/migrate.d.ts +14 -0
  31. package/dist/commands/migrate.d.ts.map +1 -0
  32. package/dist/commands/migrate.js +197 -0
  33. package/dist/commands/plugin.d.ts +10 -0
  34. package/dist/commands/plugin.d.ts.map +1 -0
  35. package/dist/commands/plugin.js +239 -0
  36. package/dist/commands/remove.d.ts +11 -0
  37. package/dist/commands/remove.d.ts.map +1 -0
  38. package/dist/commands/remove.js +472 -0
  39. package/dist/commands/secret.d.ts +12 -0
  40. package/dist/commands/secret.d.ts.map +1 -0
  41. package/dist/commands/secret.js +102 -0
  42. package/dist/commands/setup.d.ts +9 -0
  43. package/dist/commands/setup.d.ts.map +1 -0
  44. package/dist/commands/setup.js +788 -0
  45. package/dist/commands/update.d.ts +14 -0
  46. package/dist/commands/update.d.ts.map +1 -0
  47. package/dist/commands/update.js +211 -0
  48. package/dist/commands/use.d.ts +9 -0
  49. package/dist/commands/use.d.ts.map +1 -0
  50. package/dist/commands/use.js +51 -0
  51. package/dist/index.d.ts +22 -0
  52. package/dist/index.d.ts.map +1 -0
  53. package/dist/index.js +45 -0
  54. package/dist/license.d.ts +55 -0
  55. package/dist/license.d.ts.map +1 -0
  56. package/dist/license.js +258 -0
  57. package/dist/scanner.d.ts +31 -0
  58. package/dist/scanner.d.ts.map +1 -0
  59. package/dist/scanner.js +113 -0
  60. package/dist/schema-manager.d.ts +26 -0
  61. package/dist/schema-manager.d.ts.map +1 -0
  62. package/dist/schema-manager.js +132 -0
  63. package/dist/templates/admin.d.ts +49 -0
  64. package/dist/templates/admin.d.ts.map +1 -0
  65. package/dist/templates/admin.js +1358 -0
  66. package/dist/templates/ai-routes.d.ts +17 -0
  67. package/dist/templates/ai-routes.d.ts.map +1 -0
  68. package/dist/templates/ai-routes.js +1130 -0
  69. package/dist/templates/ai-service-tools.d.ts +22 -0
  70. package/dist/templates/ai-service-tools.d.ts.map +1 -0
  71. package/dist/templates/ai-service-tools.js +1424 -0
  72. package/dist/templates/ai-service.d.ts +66 -0
  73. package/dist/templates/ai-service.d.ts.map +1 -0
  74. package/dist/templates/ai-service.js +2202 -0
  75. package/dist/templates/api-routes.d.ts +108 -0
  76. package/dist/templates/api-routes.d.ts.map +1 -0
  77. package/dist/templates/api-routes.js +1219 -0
  78. package/dist/templates/auth.d.ts +48 -0
  79. package/dist/templates/auth.d.ts.map +1 -0
  80. package/dist/templates/auth.js +381 -0
  81. package/dist/templates/billing.d.ts +44 -0
  82. package/dist/templates/billing.d.ts.map +1 -0
  83. package/dist/templates/billing.js +551 -0
  84. package/dist/templates/chat.d.ts +63 -0
  85. package/dist/templates/chat.d.ts.map +1 -0
  86. package/dist/templates/chat.js +1979 -0
  87. package/dist/templates/components.d.ts +22 -0
  88. package/dist/templates/components.d.ts.map +1 -0
  89. package/dist/templates/components.js +672 -0
  90. package/dist/templates/config.d.ts +6 -0
  91. package/dist/templates/config.d.ts.map +1 -0
  92. package/dist/templates/config.js +86 -0
  93. package/dist/templates/docker.d.ts +25 -0
  94. package/dist/templates/docker.d.ts.map +1 -0
  95. package/dist/templates/docker.js +165 -0
  96. package/dist/templates/gdpr.d.ts +16 -0
  97. package/dist/templates/gdpr.d.ts.map +1 -0
  98. package/dist/templates/gdpr.js +259 -0
  99. package/dist/templates/index.d.ts +77 -0
  100. package/dist/templates/index.d.ts.map +1 -0
  101. package/dist/templates/index.js +339 -0
  102. package/dist/templates/layout.d.ts +67 -0
  103. package/dist/templates/layout.d.ts.map +1 -0
  104. package/dist/templates/layout.js +670 -0
  105. package/dist/templates/mfa.d.ts +23 -0
  106. package/dist/templates/mfa.d.ts.map +1 -0
  107. package/dist/templates/mfa.js +353 -0
  108. package/dist/templates/middleware.d.ts +12 -0
  109. package/dist/templates/middleware.d.ts.map +1 -0
  110. package/dist/templates/middleware.js +116 -0
  111. package/dist/templates/prisma.d.ts +35 -0
  112. package/dist/templates/prisma.d.ts.map +1 -0
  113. package/dist/templates/prisma.js +724 -0
  114. package/dist/templates/provider-routes.d.ts +21 -0
  115. package/dist/templates/provider-routes.d.ts.map +1 -0
  116. package/dist/templates/provider-routes.js +1203 -0
  117. package/dist/templates/rag.d.ts +48 -0
  118. package/dist/templates/rag.d.ts.map +1 -0
  119. package/dist/templates/rag.js +532 -0
  120. package/dist/templates/widget.d.ts +64 -0
  121. package/dist/templates/widget.d.ts.map +1 -0
  122. package/dist/templates/widget.js +1360 -0
  123. package/dist/utils/provider-db.d.ts +63 -0
  124. package/dist/utils/provider-db.d.ts.map +1 -0
  125. package/dist/utils/provider-db.js +300 -0
  126. package/dist/utils.d.ts +78 -0
  127. package/dist/utils.d.ts.map +1 -0
  128. package/dist/utils.js +330 -0
  129. package/package.json +60 -0
@@ -0,0 +1,1203 @@
1
+ "use strict";
2
+ /**
3
+ * Provider Route Templates
4
+ *
5
+ * Generates the complete Provider API infrastructure:
6
+ * - CRUD routes (/api/providers)
7
+ * - Single provider routes (/api/providers/[id])
8
+ * - Provider test route (/api/providers/[id]/test)
9
+ * - Internal API routes (/api/internal/providers/*)
10
+ * - Notify-provider-change utility
11
+ *
12
+ * These are CORE infrastructure — always generated, not feature-conditional.
13
+ */
14
+ Object.defineProperty(exports, "__esModule", { value: true });
15
+ exports.generateProviderCrudRoute = generateProviderCrudRoute;
16
+ exports.generateProviderIdRoute = generateProviderIdRoute;
17
+ exports.generateProviderTestRoute = generateProviderTestRoute;
18
+ exports.generateProviderSyncRoute = generateProviderSyncRoute;
19
+ exports.generateInternalProvidersRoute = generateInternalProvidersRoute;
20
+ exports.generateInternalProviderIdRoute = generateInternalProviderIdRoute;
21
+ exports.generateInternalProviderUsageRoute = generateInternalProviderUsageRoute;
22
+ exports.generateNotifyProviderChangeLib = generateNotifyProviderChangeLib;
23
+ // ─────────────────────────────────────────────────────────
24
+ // Provider CRUD Route — /api/providers/route.ts
25
+ // ─────────────────────────────────────────────────────────
26
+ function generateProviderCrudRoute() {
27
+ return `// @chimerai component=ProviderCrudRoute version=1.0
28
+ /**
29
+ * Provider CRUD API Route
30
+ * GET /api/providers — List all providers
31
+ * POST /api/providers — Create new provider
32
+ */
33
+
34
+ import { NextRequest, NextResponse } from 'next/server';
35
+ import { getServerSession } from 'next-auth';
36
+ import { authOptions } from '@/lib/auth';
37
+ import { prisma } from '@/lib/prisma';
38
+ import { encrypt } from '@/lib/encryption';
39
+ import { notifyProviderChange } from '@/lib/notify-provider-change';
40
+ import { logAuditAction } from '@/lib/audit';
41
+
42
+ /** SQLite stores Json/String[] as plain String — serialize/deserialize at runtime */
43
+ const _isSqlite = (process.env.DATABASE_URL || '').startsWith('file:');
44
+ function _toDb(v: any): any { return _isSqlite && v != null && typeof v !== 'string' ? JSON.stringify(v) : v; }
45
+ function _fromDb(v: any): any { if (!_isSqlite || typeof v !== 'string') return v; try { return JSON.parse(v); } catch { return v; } }
46
+
47
+ /** Provider types that work without an API key (e.g. local services) */
48
+ const KEYLESS_PROVIDERS = ['ollama'];
49
+
50
+ function getDefaultBaseUrl(type: string): string | null {
51
+ switch (type) {
52
+ case 'openai': return 'https://api.openai.com/v1';
53
+ case 'anthropic': return 'https://api.anthropic.com/v1';
54
+ case 'ollama': return 'http://localhost:11434';
55
+ case 'groq': return 'https://api.groq.com/openai/v1';
56
+ case 'google': return 'https://generativelanguage.googleapis.com/v1beta';
57
+ default: return null;
58
+ }
59
+ }
60
+
61
+ export async function GET(request: NextRequest) {
62
+ try {
63
+ const session = await getServerSession(authOptions);
64
+ if (!session?.user) {
65
+ return NextResponse.json({ error: 'Unauthorized' }, { status: 401 });
66
+ }
67
+
68
+ const { searchParams } = new URL(request.url);
69
+ const type = searchParams.get('type');
70
+ const status = searchParams.get('status');
71
+
72
+ const providers = await prisma.provider.findMany({
73
+ where: {
74
+ ...(type && { type }),
75
+ ...(status && { status }),
76
+ },
77
+ include: {
78
+ models: {
79
+ where: { isAvailable: true, isDeprecated: false },
80
+ select: {
81
+ id: true,
82
+ modelId: true,
83
+ name: true,
84
+ capabilities: true,
85
+ contextWindow: true,
86
+ inputCost: true,
87
+ outputCost: true,
88
+ },
89
+ },
90
+ health: true,
91
+ },
92
+ orderBy: [{ isDefault: 'desc' }, { priority: 'asc' }, { name: 'asc' }],
93
+ });
94
+
95
+ // Don't expose encrypted API keys in list view
96
+ // Deserialize JSON-string fields (SQLite stores Json/String[] as plain String)
97
+ const sanitized = providers.map((p: any) => ({
98
+ ...p,
99
+ apiKey: p.apiKey ? '***encrypted***' : null,
100
+ config: _fromDb(p.config),
101
+ tags: _fromDb(p.tags),
102
+ models: p.models?.map((m: any) => ({ ...m, capabilities: _fromDb(m.capabilities) })),
103
+ }));
104
+
105
+ return NextResponse.json(sanitized);
106
+ } catch (error: any) {
107
+ console.error('Error fetching providers:', error);
108
+ return NextResponse.json(
109
+ { error: 'Failed to fetch providers', details: error.message },
110
+ { status: 500 }
111
+ );
112
+ }
113
+ }
114
+
115
+ export async function POST(request: NextRequest) {
116
+ try {
117
+ const session = await getServerSession(authOptions);
118
+ if (!session?.user) {
119
+ return NextResponse.json({ error: 'Unauthorized' }, { status: 401 });
120
+ }
121
+
122
+ const body = await request.json();
123
+
124
+ // Validate API key based on provider type
125
+ const requiresKey = !KEYLESS_PROVIDERS.includes(body.type);
126
+ if (requiresKey && !body.config?.apiKey) {
127
+ return NextResponse.json(
128
+ { error: 'API key is required for this provider type' },
129
+ { status: 400 }
130
+ );
131
+ }
132
+
133
+ // Key verschluesseln wenn vorhanden, sonst null (nicht leerer String!)
134
+ const encryptedKey = body.config?.apiKey ? encrypt(body.config.apiKey) : null;
135
+
136
+ // Resolve baseUrl
137
+ const resolvedBaseUrl = body.config?.baseUrl || getDefaultBaseUrl(body.type);
138
+
139
+ // If setting as default, unset other defaults of same type
140
+ if (body.isDefault) {
141
+ await prisma.provider.updateMany({
142
+ where: { type: body.type, isDefault: true },
143
+ data: { isDefault: false },
144
+ });
145
+ }
146
+
147
+ const provider = await prisma.provider.create({
148
+ data: {
149
+ name: body.name,
150
+ type: body.type,
151
+ description: body.description,
152
+ baseUrl: resolvedBaseUrl,
153
+ apiKey: encryptedKey,
154
+ config: _toDb({ ...body.config, apiKey: undefined }),
155
+ status: 'active',
156
+ isDefault: body.isDefault || false,
157
+ priority: body.priority || 0,
158
+ tags: _toDb(body.tags || []),
159
+ createdBy: session.user.id,
160
+ },
161
+ include: { models: true, health: true },
162
+ });
163
+
164
+ // Notify AI service to invalidate cache
165
+ notifyProviderChange().catch(() => {});
166
+
167
+ // Audit log
168
+ await logAuditAction({
169
+ action: 'provider.create',
170
+ userId: session.user.id,
171
+ targetType: 'provider',
172
+ targetId: provider.id,
173
+ metadata: { name: provider.name, type: provider.type },
174
+ });
175
+
176
+ return NextResponse.json(
177
+ { ...provider, apiKey: '***encrypted***' },
178
+ { status: 201 }
179
+ );
180
+ } catch (error: any) {
181
+ console.error('Error creating provider:', error);
182
+ return NextResponse.json(
183
+ { error: 'Failed to create provider', details: error.message },
184
+ { status: 500 }
185
+ );
186
+ }
187
+ }
188
+ `;
189
+ }
190
+ // ─────────────────────────────────────────────────────────
191
+ // Provider [id] Route — /api/providers/[id]/route.ts
192
+ // ─────────────────────────────────────────────────────────
193
+ function generateProviderIdRoute() {
194
+ return `// @chimerai component=ProviderIdRoute version=1.0
195
+ /**
196
+ * Single Provider API Route
197
+ * GET /api/providers/[id] — Get provider details
198
+ * PUT /api/providers/[id] — Full update
199
+ * PATCH /api/providers/[id] — Partial update
200
+ * DELETE /api/providers/[id] — Delete provider
201
+ */
202
+
203
+ import { NextRequest, NextResponse } from 'next/server';
204
+ import { getServerSession } from 'next-auth';
205
+ import { authOptions } from '@/lib/auth';
206
+ import { prisma } from '@/lib/prisma';
207
+ import { encrypt } from '@/lib/encryption';
208
+ import { notifyProviderChange } from '@/lib/notify-provider-change';
209
+ import { logAuditAction } from '@/lib/audit';
210
+
211
+ /** SQLite stores Json/String[] as plain String — serialize/deserialize at runtime */
212
+ const _isSqlite = (process.env.DATABASE_URL || '').startsWith('file:');
213
+ function _toDb(v: any): any { return _isSqlite && v != null && typeof v !== 'string' ? JSON.stringify(v) : v; }
214
+ function _fromDb(v: any): any { if (!_isSqlite || typeof v !== 'string') return v; try { return JSON.parse(v); } catch { return v; } }
215
+
216
+ function getDefaultBaseUrl(type: string): string | null {
217
+ switch (type) {
218
+ case 'openai': return 'https://api.openai.com/v1';
219
+ case 'anthropic': return 'https://api.anthropic.com/v1';
220
+ case 'ollama': return 'http://localhost:11434';
221
+ case 'groq': return 'https://api.groq.com/openai/v1';
222
+ case 'google': return 'https://generativelanguage.googleapis.com/v1beta';
223
+ default: return null;
224
+ }
225
+ }
226
+
227
+ export async function GET(
228
+ request: NextRequest,
229
+ { params }: { params: Promise<{ id: string }> }
230
+ ) {
231
+ const { id } = await params;
232
+ try {
233
+ const session = await getServerSession(authOptions);
234
+ if (!session?.user) {
235
+ return NextResponse.json({ error: 'Unauthorized' }, { status: 401 });
236
+ }
237
+
238
+ const provider = await prisma.provider.findUnique({
239
+ where: { id: id },
240
+ include: {
241
+ models: { orderBy: { name: 'asc' } },
242
+ health: true,
243
+ },
244
+ });
245
+
246
+ if (!provider) {
247
+ return NextResponse.json({ error: 'Provider not found' }, { status: 404 });
248
+ }
249
+
250
+ return NextResponse.json({
251
+ ...provider,
252
+ apiKey: provider.apiKey ? provider.apiKey.substring(0, 8) + '...' : null,
253
+ config: _fromDb(provider.config),
254
+ tags: _fromDb(provider.tags),
255
+ models: provider.models?.map((m: any) => ({ ...m, capabilities: _fromDb(m.capabilities) })),
256
+ });
257
+ } catch (error: any) {
258
+ console.error('Error fetching provider:', error);
259
+ return NextResponse.json(
260
+ { error: 'Failed to fetch provider', details: error.message },
261
+ { status: 500 }
262
+ );
263
+ }
264
+ }
265
+
266
+ export async function PUT(
267
+ request: NextRequest,
268
+ { params }: { params: Promise<{ id: string }> }
269
+ ) {
270
+ const { id } = await params;
271
+ try {
272
+ const session = await getServerSession(authOptions);
273
+ if (!session?.user) {
274
+ return NextResponse.json({ error: 'Unauthorized' }, { status: 401 });
275
+ }
276
+
277
+ const body = await request.json();
278
+
279
+ // If setting as default, unset other defaults
280
+ if (body.isDefault) {
281
+ const existing = await prisma.provider.findUnique({ where: { id: id } });
282
+ if (existing) {
283
+ await prisma.provider.updateMany({
284
+ where: { type: existing.type, isDefault: true, NOT: { id: id } },
285
+ data: { isDefault: false },
286
+ });
287
+ }
288
+ }
289
+
290
+ // Handle API key update
291
+ const updateData: any = { ...body };
292
+ if (body.config?.apiKey) {
293
+ updateData.apiKey = encrypt(body.config.apiKey);
294
+ updateData.config = _toDb({ ...body.config, apiKey: undefined });
295
+ } else if (body.config) {
296
+ updateData.config = _toDb({ ...body.config, apiKey: undefined });
297
+ }
298
+ if ('tags' in body) updateData.tags = _toDb(body.tags);
299
+
300
+ const provider = await prisma.provider.update({
301
+ where: { id: id },
302
+ data: updateData,
303
+ include: { models: true, health: true },
304
+ });
305
+
306
+ notifyProviderChange().catch(() => {});
307
+
308
+ // Audit log
309
+ await logAuditAction({
310
+ action: 'provider.update',
311
+ userId: session.user.id,
312
+ targetType: 'provider',
313
+ targetId: id,
314
+ metadata: { name: provider.name, type: provider.type },
315
+ });
316
+
317
+ return NextResponse.json({ ...provider, apiKey: '***encrypted***' });
318
+ } catch (error: any) {
319
+ console.error('Error updating provider:', error);
320
+ return NextResponse.json(
321
+ { error: 'Failed to update provider', details: error.message },
322
+ { status: 500 }
323
+ );
324
+ }
325
+ }
326
+
327
+ export async function PATCH(
328
+ request: NextRequest,
329
+ { params }: { params: Promise<{ id: string }> }
330
+ ) {
331
+ const { id } = await params;
332
+ try {
333
+ const session = await getServerSession(authOptions);
334
+ if (!session?.user) {
335
+ return NextResponse.json({ error: 'Unauthorized' }, { status: 401 });
336
+ }
337
+
338
+ const body = await request.json();
339
+ const updateData: any = {};
340
+
341
+ // Handle simple field updates
342
+ if ('status' in body) updateData.status = body.status;
343
+ if ('isDefault' in body) updateData.isDefault = body.isDefault;
344
+ if ('priority' in body) updateData.priority = body.priority;
345
+ if ('tags' in body) updateData.tags = _toDb(body.tags);
346
+ if ('name' in body) updateData.name = body.name;
347
+ if ('type' in body) updateData.type = body.type;
348
+ if ('description' in body) updateData.description = body.description;
349
+
350
+ // Handle baseUrl
351
+ if ('baseUrl' in body) {
352
+ updateData.baseUrl = body.baseUrl;
353
+ } else if (body.config?.baseUrl !== undefined) {
354
+ updateData.baseUrl = body.config.baseUrl || getDefaultBaseUrl(body.type || '');
355
+ }
356
+
357
+ // Handle API key update (encrypted)
358
+ if (body.config?.apiKey) {
359
+ updateData.apiKey = encrypt(body.config.apiKey);
360
+ }
361
+
362
+ if (body.config) {
363
+ updateData.config = _toDb({ ...body.config, apiKey: undefined });
364
+ }
365
+
366
+ // If setting as default, unset other defaults
367
+ if (body.isDefault) {
368
+ const existing = await prisma.provider.findUnique({ where: { id: id } });
369
+ if (existing) {
370
+ await prisma.provider.updateMany({
371
+ where: { type: existing.type, isDefault: true, NOT: { id: id } },
372
+ data: { isDefault: false },
373
+ });
374
+ }
375
+ }
376
+
377
+ const provider = await prisma.provider.update({
378
+ where: { id: id },
379
+ data: updateData,
380
+ include: { models: true, health: true },
381
+ });
382
+
383
+ notifyProviderChange().catch(() => {});
384
+
385
+ // Audit log
386
+ await logAuditAction({
387
+ action: 'provider.update',
388
+ userId: session.user.id,
389
+ targetType: 'provider',
390
+ targetId: id,
391
+ metadata: { name: provider.name, type: provider.type },
392
+ });
393
+
394
+ return NextResponse.json({
395
+ ...provider,
396
+ apiKey: provider.apiKey ? '***encrypted***' : null,
397
+ });
398
+ } catch (error: any) {
399
+ console.error('Error patching provider:', error);
400
+ return NextResponse.json(
401
+ { error: 'Failed to update provider', details: error.message },
402
+ { status: 500 }
403
+ );
404
+ }
405
+ }
406
+
407
+ export async function DELETE(
408
+ request: NextRequest,
409
+ { params }: { params: Promise<{ id: string }> }
410
+ ) {
411
+ const { id } = await params;
412
+ try {
413
+ const session = await getServerSession(authOptions);
414
+ if (!session?.user) {
415
+ return NextResponse.json({ error: 'Unauthorized' }, { status: 401 });
416
+ }
417
+
418
+ await prisma.provider.delete({ where: { id: id } });
419
+ notifyProviderChange().catch(() => {});
420
+
421
+ // Audit log
422
+ await logAuditAction({
423
+ action: 'provider.delete',
424
+ userId: session.user.id,
425
+ targetType: 'provider',
426
+ targetId: id,
427
+ });
428
+
429
+ return NextResponse.json({ success: true });
430
+ } catch (error: any) {
431
+ console.error('Error deleting provider:', error);
432
+ return NextResponse.json(
433
+ { error: 'Failed to delete provider', details: error.message },
434
+ { status: 500 }
435
+ );
436
+ }
437
+ }
438
+ `;
439
+ }
440
+ // ─────────────────────────────────────────────────────────
441
+ // Provider Test Route — /api/providers/[id]/test/route.ts
442
+ // ─────────────────────────────────────────────────────────
443
+ function generateProviderTestRoute() {
444
+ return `// @chimerai component=ProviderTestRoute version=1.0
445
+ /**
446
+ * Provider Test API Route
447
+ * POST /api/providers/[id]/test — Test provider connection
448
+ */
449
+
450
+ import { NextRequest, NextResponse } from 'next/server';
451
+ import { getServerSession } from 'next-auth';
452
+ import { authOptions } from '@/lib/auth';
453
+ import { prisma } from '@/lib/prisma';
454
+ import { decrypt } from '@/lib/encryption';
455
+
456
+ /** Provider types that work without an API key (e.g. local services) */
457
+ const KEYLESS_PROVIDERS = ['ollama'];
458
+
459
+ function getDefaultBaseUrl(type: string): string {
460
+ switch (type) {
461
+ case 'openai': return 'https://api.openai.com/v1';
462
+ case 'anthropic': return 'https://api.anthropic.com/v1';
463
+ case 'ollama': return 'http://localhost:11434';
464
+ case 'groq': return 'https://api.groq.com/openai/v1';
465
+ case 'google': return 'https://generativelanguage.googleapis.com/v1beta';
466
+ default: return 'https://api.openai.com/v1';
467
+ }
468
+ }
469
+
470
+ export async function POST(
471
+ request: NextRequest,
472
+ { params }: { params: Promise<{ id: string }> }
473
+ ) {
474
+ const { id } = await params;
475
+ try {
476
+ const session = await getServerSession(authOptions);
477
+ if (!session?.user) {
478
+ return NextResponse.json({ error: 'Unauthorized' }, { status: 401 });
479
+ }
480
+
481
+ const provider = await prisma.provider.findUnique({
482
+ where: { id: id },
483
+ });
484
+
485
+ if (!provider) {
486
+ return NextResponse.json({ error: 'Provider not found' }, { status: 404 });
487
+ }
488
+
489
+ const baseUrl = provider.baseUrl || getDefaultBaseUrl(provider.type);
490
+ const startTime = Date.now();
491
+ let result: any = { success: false, responseTime: 0, details: {} };
492
+
493
+ // Decrypt API key — keyless providers (e.g. Ollama) skip this
494
+ const requiresKey = !KEYLESS_PROVIDERS.includes(provider.type);
495
+ let apiKey: string | null = null;
496
+
497
+ if (provider.apiKey) {
498
+ try {
499
+ apiKey = decrypt(provider.apiKey);
500
+ } catch (decryptError: any) {
501
+ return NextResponse.json({
502
+ success: false,
503
+ responseTime: 0,
504
+ errorMessage: 'Failed to decrypt API key. Please re-enter the API key for this provider.',
505
+ details: {},
506
+ }, { status: 500 });
507
+ }
508
+ }
509
+
510
+ if (requiresKey && !apiKey) {
511
+ return NextResponse.json({
512
+ success: false,
513
+ responseTime: 0,
514
+ errorMessage: 'No API key configured. Please edit the provider and add an API key.',
515
+ details: {},
516
+ }, { status: 400 });
517
+ }
518
+
519
+ // At this point apiKey is either a valid string or null (for keyless providers)
520
+ const resolvedKey = apiKey ?? '';
521
+
522
+ try {
523
+
524
+ switch (provider.type) {
525
+ case 'openai': {
526
+ const resp = await fetch(\`\${baseUrl}/models\`, {
527
+ headers: { Authorization: \`Bearer \${resolvedKey}\` },
528
+ signal: AbortSignal.timeout(10000),
529
+ });
530
+ const responseTime = Date.now() - startTime;
531
+ if (resp.ok) {
532
+ const data = await resp.json();
533
+ result = { success: true, responseTime, details: { chatTest: true, modelsFound: data.data?.length || 0 } };
534
+ } else {
535
+ throw new Error(\`API returned \${resp.status}: \${resp.statusText}\`);
536
+ }
537
+ break;
538
+ }
539
+
540
+ case 'anthropic': {
541
+ const resp = await fetch(\`\${baseUrl}/messages\`, {
542
+ method: 'POST',
543
+ headers: {
544
+ 'x-api-key': resolvedKey,
545
+ 'anthropic-version': '2023-06-01',
546
+ 'Content-Type': 'application/json',
547
+ },
548
+ body: JSON.stringify({
549
+ model: 'claude-3-haiku-20240307',
550
+ max_tokens: 10,
551
+ messages: [{ role: 'user', content: 'Hi' }],
552
+ }),
553
+ signal: AbortSignal.timeout(10000),
554
+ });
555
+ const responseTime = Date.now() - startTime;
556
+ if (resp.ok) {
557
+ result = { success: true, responseTime, details: { chatTest: true } };
558
+ } else {
559
+ throw new Error(\`API returned \${resp.status}: \${resp.statusText}\`);
560
+ }
561
+ break;
562
+ }
563
+
564
+ case 'ollama': {
565
+ const resp = await fetch(\`\${baseUrl}/api/tags\`, {
566
+ signal: AbortSignal.timeout(10000),
567
+ });
568
+ const responseTime = Date.now() - startTime;
569
+ if (resp.ok) {
570
+ const data = await resp.json();
571
+ result = { success: true, responseTime, details: { chatTest: true, modelsFound: data.models?.length || 0 } };
572
+ } else {
573
+ throw new Error(\`API returned \${resp.status}: \${resp.statusText}\`);
574
+ }
575
+ break;
576
+ }
577
+
578
+ default: {
579
+ const resp = await fetch(\`\${baseUrl}/models\`, {
580
+ headers: { Authorization: \`Bearer \${resolvedKey}\` },
581
+ signal: AbortSignal.timeout(10000),
582
+ });
583
+ const responseTime = Date.now() - startTime;
584
+ result = { success: resp.ok, responseTime, details: { chatTest: resp.ok } };
585
+ if (!resp.ok) throw new Error(\`API returned \${resp.status}: \${resp.statusText}\`);
586
+ }
587
+ }
588
+
589
+ // Update status + health
590
+ await prisma.provider.update({
591
+ where: { id: id },
592
+ data: { status: 'active' },
593
+ });
594
+
595
+ await prisma.providerHealth.upsert({
596
+ where: { providerId: id },
597
+ create: {
598
+ providerId: id, status: 'healthy',
599
+ responseTime: result.responseTime, lastCheck: new Date(),
600
+ modelsAvailable: result.details.modelsFound || 0,
601
+ chatAvailable: result.details.chatTest || false,
602
+ apiKeyValid: true,
603
+ },
604
+ update: {
605
+ status: 'healthy', responseTime: result.responseTime,
606
+ lastCheck: new Date(), modelsAvailable: result.details.modelsFound || 0,
607
+ chatAvailable: result.details.chatTest || false,
608
+ apiKeyValid: true, errorMessage: null,
609
+ },
610
+ });
611
+
612
+ return NextResponse.json(result);
613
+ } catch (error: any) {
614
+ const responseTime = Date.now() - startTime;
615
+ result = { success: false, responseTime, errorMessage: error.message, details: {} };
616
+
617
+ await prisma.provider.update({
618
+ where: { id: id },
619
+ data: { status: 'error' },
620
+ });
621
+
622
+ await prisma.providerHealth.upsert({
623
+ where: { providerId: id },
624
+ create: {
625
+ providerId: id, status: 'unhealthy', responseTime,
626
+ lastCheck: new Date(), errorMessage: error.message,
627
+ modelsAvailable: 0, chatAvailable: false, apiKeyValid: false,
628
+ },
629
+ update: {
630
+ status: 'unhealthy', responseTime, lastCheck: new Date(),
631
+ errorMessage: error.message, chatAvailable: false, apiKeyValid: false,
632
+ },
633
+ });
634
+
635
+ return NextResponse.json(result, { status: 500 });
636
+ }
637
+ } catch (error: any) {
638
+ console.error('Error testing provider:', error);
639
+ return NextResponse.json(
640
+ { error: 'Failed to test provider', details: error.message },
641
+ { status: 500 }
642
+ );
643
+ }
644
+ }
645
+ `;
646
+ }
647
+ // ─────────────────────────────────────────────────────────
648
+ // Provider Sync Route — /api/providers/[id]/sync/route.ts
649
+ // Fetches models from the provider API and upserts them into the DB
650
+ // ─────────────────────────────────────────────────────────
651
+ function generateProviderSyncRoute() {
652
+ return `// @chimerai component=ProviderSyncRoute version=1.0
653
+ /**
654
+ * Provider Sync API Route
655
+ * POST /api/providers/[id]/sync — Fetch models from provider API and save to DB
656
+ */
657
+
658
+ import { NextRequest, NextResponse } from 'next/server';
659
+ import { getServerSession } from 'next-auth';
660
+ import { authOptions } from '@/lib/auth';
661
+ import { prisma } from '@/lib/prisma';
662
+ import { decrypt } from '@/lib/encryption';
663
+
664
+ /** SQLite stores Json/String[] as plain String — serialize at runtime */
665
+ const _isSqlite = (process.env.DATABASE_URL || '').startsWith('file:');
666
+ function _toDb(v: any): any { return _isSqlite && v != null && typeof v !== 'string' ? JSON.stringify(v) : v; }
667
+
668
+ const KEYLESS_PROVIDERS = ['ollama'];
669
+
670
+ function getDefaultBaseUrl(type: string): string {
671
+ switch (type) {
672
+ case 'openai': return 'https://api.openai.com/v1';
673
+ case 'anthropic': return 'https://api.anthropic.com/v1';
674
+ case 'ollama': return 'http://localhost:11434';
675
+ case 'groq': return 'https://api.groq.com/openai/v1';
676
+ case 'google': return 'https://generativelanguage.googleapis.com/v1beta';
677
+ default: return 'https://api.openai.com/v1';
678
+ }
679
+ }
680
+
681
+ /** Well-known model metadata for cost/context enrichment */
682
+ const MODEL_METADATA: Record<string, { contextWindow?: number; inputCost?: number; outputCost?: number; capabilities?: string[] }> = {
683
+ 'gpt-4o': { contextWindow: 128000, inputCost: 2.5, outputCost: 10, capabilities: ['chat', 'vision'] },
684
+ 'gpt-4o-mini': { contextWindow: 128000, inputCost: 0.15, outputCost: 0.6, capabilities: ['chat'] },
685
+ 'gpt-4-turbo': { contextWindow: 128000, inputCost: 10, outputCost: 30, capabilities: ['chat', 'vision'] },
686
+ 'gpt-4': { contextWindow: 8192, inputCost: 30, outputCost: 60, capabilities: ['chat'] },
687
+ 'gpt-3.5-turbo': { contextWindow: 16385, inputCost: 0.5, outputCost: 1.5, capabilities: ['chat'] },
688
+ 'o1': { contextWindow: 200000, inputCost: 15, outputCost: 60, capabilities: ['chat'] },
689
+ 'o1-mini': { contextWindow: 128000, inputCost: 3, outputCost: 12, capabilities: ['chat'] },
690
+ 'o3-mini': { contextWindow: 200000, inputCost: 1.1, outputCost: 4.4, capabilities: ['chat'] },
691
+ 'text-embedding-3-small': { contextWindow: 8191, inputCost: 0.02, outputCost: 0, capabilities: ['embedding'] },
692
+ 'text-embedding-3-large': { contextWindow: 8191, inputCost: 0.13, outputCost: 0, capabilities: ['embedding'] },
693
+ 'claude-sonnet-4-20250514': { contextWindow: 200000, inputCost: 3, outputCost: 15, capabilities: ['chat', 'vision'] },
694
+ 'claude-3-5-sonnet-20241022':{ contextWindow: 200000, inputCost: 3, outputCost: 15, capabilities: ['chat', 'vision'] },
695
+ 'claude-3-haiku-20240307': { contextWindow: 200000, inputCost: 0.25, outputCost: 1.25, capabilities: ['chat'] },
696
+ 'claude-3-opus-20240229': { contextWindow: 200000, inputCost: 15, outputCost: 75, capabilities: ['chat', 'vision'] },
697
+ };
698
+
699
+ /** Filter: only keep models useful for chat/embedding */
700
+ function isUsefulModel(modelId: string, providerType: string): boolean {
701
+ if (providerType === 'ollama') return true;
702
+ if (providerType === 'anthropic') return true;
703
+ if (providerType === 'openai' || providerType === 'groq') {
704
+ if (modelId.startsWith('ft:')) return false;
705
+ if (/^(whisper|tts|dall-e|babbage|davinci|canary)/.test(modelId)) return false;
706
+ return /^(gpt-|o1|o3|o4|text-embedding|chatgpt)/.test(modelId);
707
+ }
708
+ return true;
709
+ }
710
+
711
+ function humanName(modelId: string): string {
712
+ return modelId
713
+ .replace(/-/g, ' ')
714
+ .replace(/\\b\\w/g, (c: string) => c.toUpperCase())
715
+ .replace(/Gpt /g, 'GPT ')
716
+ .replace(/^O1/g, 'O1')
717
+ .replace(/^O3/g, 'O3');
718
+ }
719
+
720
+ async function fetchOpenAIModels(baseUrl: string, apiKey: string) {
721
+ const resp = await fetch(baseUrl + '/models', {
722
+ headers: { Authorization: 'Bearer ' + apiKey },
723
+ signal: AbortSignal.timeout(15000),
724
+ });
725
+ if (!resp.ok) throw new Error('OpenAI API returned ' + resp.status);
726
+ const data = await resp.json();
727
+ return (data.data || []).map((m: any) => ({ modelId: m.id, name: humanName(m.id) }));
728
+ }
729
+
730
+ async function fetchAnthropicModels() {
731
+ // Anthropic has no /models endpoint — return well-known models
732
+ return [
733
+ { modelId: 'claude-sonnet-4-20250514', name: 'Claude Sonnet 4' },
734
+ { modelId: 'claude-3-5-sonnet-20241022', name: 'Claude 3.5 Sonnet' },
735
+ { modelId: 'claude-3-haiku-20240307', name: 'Claude 3 Haiku' },
736
+ { modelId: 'claude-3-opus-20240229', name: 'Claude 3 Opus' },
737
+ ];
738
+ }
739
+
740
+ async function fetchOllamaModels(baseUrl: string) {
741
+ const resp = await fetch(baseUrl + '/api/tags', { signal: AbortSignal.timeout(10000) });
742
+ if (!resp.ok) throw new Error('Ollama API returned ' + resp.status);
743
+ const data = await resp.json();
744
+ return (data.models || []).map((m: any) => ({
745
+ modelId: m.name || m.model,
746
+ name: (m.name || m.model).replace(/:latest$/, ''),
747
+ }));
748
+ }
749
+
750
+ export async function POST(
751
+ request: NextRequest,
752
+ { params }: { params: Promise<{ id: string }> }
753
+ ) {
754
+ const { id } = await params;
755
+ try {
756
+ const session = await getServerSession(authOptions);
757
+ if (!session?.user) {
758
+ return NextResponse.json({ error: 'Unauthorized' }, { status: 401 });
759
+ }
760
+
761
+ const provider = await prisma.provider.findUnique({ where: { id: id } });
762
+ if (!provider) {
763
+ return NextResponse.json({ error: 'Provider not found' }, { status: 404 });
764
+ }
765
+
766
+ const baseUrl = provider.baseUrl || getDefaultBaseUrl(provider.type);
767
+ const requiresKey = !KEYLESS_PROVIDERS.includes(provider.type);
768
+
769
+ let apiKey: string | null = null;
770
+ if (provider.apiKey) {
771
+ try { apiKey = decrypt(provider.apiKey); } catch {
772
+ return NextResponse.json({ error: 'Failed to decrypt API key' }, { status: 500 });
773
+ }
774
+ }
775
+ if (requiresKey && !apiKey) {
776
+ return NextResponse.json({ error: 'No API key configured' }, { status: 400 });
777
+ }
778
+
779
+ // Fetch models from provider API
780
+ let rawModels: { modelId: string; name: string }[] = [];
781
+ switch (provider.type) {
782
+ case 'openai':
783
+ case 'groq':
784
+ rawModels = await fetchOpenAIModels(baseUrl, apiKey || '');
785
+ break;
786
+ case 'anthropic':
787
+ rawModels = await fetchAnthropicModels();
788
+ break;
789
+ case 'ollama':
790
+ rawModels = await fetchOllamaModels(baseUrl);
791
+ break;
792
+ default:
793
+ try { rawModels = await fetchOpenAIModels(baseUrl, apiKey || ''); } catch { rawModels = []; }
794
+ }
795
+
796
+ // Filter useful models
797
+ const models = rawModels.filter(m => isUsefulModel(m.modelId, provider.type));
798
+
799
+ // Upsert models into DB
800
+ let created = 0;
801
+ let updated = 0;
802
+ for (const m of models) {
803
+ const meta = MODEL_METADATA[m.modelId] || {};
804
+ const capabilities = meta.capabilities || ['chat'];
805
+ const existing = await (prisma as any).model.findFirst({
806
+ where: { providerId: provider.id, modelId: m.modelId },
807
+ });
808
+ if (existing) {
809
+ await (prisma as any).model.update({
810
+ where: { id: existing.id },
811
+ data: { name: m.name, isAvailable: true, isDeprecated: false },
812
+ });
813
+ updated++;
814
+ } else {
815
+ await (prisma as any).model.create({
816
+ data: {
817
+ providerId: provider.id,
818
+ modelId: m.modelId,
819
+ name: m.name,
820
+ capabilities: _toDb(capabilities),
821
+ contextWindow: meta.contextWindow || 4096,
822
+ inputCost: meta.inputCost || 0,
823
+ outputCost: meta.outputCost || 0,
824
+ isAvailable: true,
825
+ },
826
+ });
827
+ created++;
828
+ }
829
+ }
830
+
831
+ return NextResponse.json({
832
+ success: true,
833
+ synced: models.length,
834
+ created,
835
+ updated,
836
+ models: models.map(m => m.modelId),
837
+ });
838
+ } catch (error: any) {
839
+ console.error('Error syncing models:', error);
840
+ return NextResponse.json(
841
+ { error: 'Failed to sync models', details: error.message },
842
+ { status: 500 }
843
+ );
844
+ }
845
+ }
846
+ `;
847
+ }
848
+ // ─────────────────────────────────────────────────────────
849
+ // Internal Providers Route — /api/internal/providers/route.ts
850
+ // ─────────────────────────────────────────────────────────
851
+ function generateInternalProvidersRoute() {
852
+ return `// @chimerai component=InternalProvidersRoute version=1.0
853
+ /**
854
+ * Internal Provider API — List all active providers
855
+ * GET /api/internal/providers
856
+ *
857
+ * Used by the Python AI Service to fetch provider configs + decrypted API keys.
858
+ * Protected by INTERNAL_API_TOKEN (Bearer token).
859
+ *
860
+ * WARNING: Returns DECRYPTED API keys — never expose externally!
861
+ */
862
+
863
+ import { NextRequest, NextResponse } from 'next/server';
864
+ import { prisma } from '@/lib/prisma';
865
+ import { decrypt } from '@/lib/encryption';
866
+
867
+ /** SQLite stores Json/String[] as plain String — deserialize at runtime */
868
+ const _isSqlite = (process.env.DATABASE_URL || '').startsWith('file:');
869
+ function _fromDb(v: any): any { if (!_isSqlite || typeof v !== 'string') return v; try { return JSON.parse(v); } catch { return v; } }
870
+
871
+ function validateInternalToken(request: NextRequest): boolean {
872
+ const authHeader = request.headers.get('authorization') || '';
873
+ const token = authHeader.replace(/^Bearer\\s+/i, '');
874
+ const expected = process.env.INTERNAL_API_TOKEN;
875
+
876
+ if (!expected || expected.length < 32) {
877
+ console.error('INTERNAL_API_TOKEN is not configured or too short');
878
+ return false;
879
+ }
880
+
881
+ return token === expected;
882
+ }
883
+
884
+ export async function GET(request: NextRequest) {
885
+ if (!validateInternalToken(request)) {
886
+ return NextResponse.json({ error: 'Unauthorized' }, { status: 401 });
887
+ }
888
+
889
+ try {
890
+ const { searchParams } = new URL(request.url);
891
+ const type = searchParams.get('type');
892
+ const status = searchParams.get('status') || 'active';
893
+
894
+ const providers = await prisma.provider.findMany({
895
+ where: {
896
+ ...(status && { status }),
897
+ ...(type && { type }),
898
+ },
899
+ include: {
900
+ models: {
901
+ where: { isAvailable: true, isDeprecated: false },
902
+ select: {
903
+ id: true, modelId: true, name: true,
904
+ capabilities: true, contextWindow: true,
905
+ inputCost: true, outputCost: true,
906
+ },
907
+ },
908
+ },
909
+ orderBy: [{ isDefault: 'desc' }, { priority: 'asc' }],
910
+ });
911
+
912
+ const result = providers.map((p: any) => {
913
+ let apiKey: string | null = null;
914
+ try {
915
+ if (p.apiKey) apiKey = decrypt(p.apiKey);
916
+ } catch {
917
+ console.warn(\`Failed to decrypt API key for provider \${p.id}\`);
918
+ }
919
+
920
+ return {
921
+ id: p.id,
922
+ name: p.name,
923
+ type: p.type,
924
+ base_url: p.baseUrl,
925
+ api_key: apiKey,
926
+ config: _fromDb(p.config),
927
+ status: p.status,
928
+ is_default: p.isDefault,
929
+ priority: p.priority,
930
+ models: p.models.map((m: any) => ({
931
+ id: m.id,
932
+ model_id: m.modelId,
933
+ name: m.name,
934
+ capabilities: _fromDb(m.capabilities),
935
+ context_window: m.contextWindow,
936
+ input_cost: m.inputCost,
937
+ output_cost: m.outputCost,
938
+ })),
939
+ };
940
+ });
941
+
942
+ return NextResponse.json({ providers: result });
943
+ } catch (error: any) {
944
+ console.error('Error fetching internal providers:', error);
945
+ return NextResponse.json(
946
+ { error: 'Failed to fetch providers', details: error.message },
947
+ { status: 500 }
948
+ );
949
+ }
950
+ }
951
+ `;
952
+ }
953
+ // ─────────────────────────────────────────────────────────
954
+ // Internal Provider [id] Route — /api/internal/providers/[id]/route.ts
955
+ // ─────────────────────────────────────────────────────────
956
+ function generateInternalProviderIdRoute() {
957
+ return `// @chimerai component=InternalProviderIdRoute version=1.0
958
+ /**
959
+ * Internal Provider API — Get single provider by ID
960
+ * GET /api/internal/providers/[id]
961
+ *
962
+ * Returns full provider details with DECRYPTED API key.
963
+ * Protected by INTERNAL_API_TOKEN (Bearer token).
964
+ */
965
+
966
+ import { NextRequest, NextResponse } from 'next/server';
967
+ import { prisma } from '@/lib/prisma';
968
+ import { decrypt } from '@/lib/encryption';
969
+
970
+ /** SQLite stores Json/String[] as plain String — deserialize at runtime */
971
+ const _isSqlite = (process.env.DATABASE_URL || '').startsWith('file:');
972
+ function _fromDb(v: any): any { if (!_isSqlite || typeof v !== 'string') return v; try { return JSON.parse(v); } catch { return v; } }
973
+
974
+ function validateInternalToken(request: NextRequest): boolean {
975
+ const authHeader = request.headers.get('authorization') || '';
976
+ const token = authHeader.replace(/^Bearer\\s+/i, '');
977
+ const expected = process.env.INTERNAL_API_TOKEN;
978
+ if (!expected || expected.length < 32) return false;
979
+ return token === expected;
980
+ }
981
+
982
+ export async function GET(
983
+ request: NextRequest,
984
+ { params }: { params: Promise<{ id: string }> }
985
+ ) {
986
+ const { id } = await params;
987
+ if (!validateInternalToken(request)) {
988
+ return NextResponse.json({ error: 'Unauthorized' }, { status: 401 });
989
+ }
990
+
991
+ try {
992
+ const provider = await prisma.provider.findUnique({
993
+ where: { id: id },
994
+ include: {
995
+ models: {
996
+ where: { isAvailable: true, isDeprecated: false },
997
+ select: {
998
+ id: true, modelId: true, name: true,
999
+ capabilities: true, contextWindow: true,
1000
+ inputCost: true, outputCost: true,
1001
+ },
1002
+ },
1003
+ },
1004
+ });
1005
+
1006
+ if (!provider) {
1007
+ return NextResponse.json({ error: 'Provider not found' }, { status: 404 });
1008
+ }
1009
+
1010
+ let apiKey: string | null = null;
1011
+ try {
1012
+ if (provider.apiKey) apiKey = decrypt(provider.apiKey);
1013
+ } catch {
1014
+ console.warn(\`Failed to decrypt API key for provider \${provider.id}\`);
1015
+ }
1016
+
1017
+ return NextResponse.json({
1018
+ id: provider.id,
1019
+ name: provider.name,
1020
+ type: provider.type,
1021
+ base_url: provider.baseUrl,
1022
+ api_key: apiKey,
1023
+ config: _fromDb(provider.config),
1024
+ status: provider.status,
1025
+ is_default: provider.isDefault,
1026
+ priority: provider.priority,
1027
+ models: provider.models.map((m: any) => ({
1028
+ id: m.id,
1029
+ model_id: m.modelId,
1030
+ name: m.name,
1031
+ capabilities: _fromDb(m.capabilities),
1032
+ context_window: m.contextWindow,
1033
+ input_cost: m.inputCost,
1034
+ output_cost: m.outputCost,
1035
+ })),
1036
+ });
1037
+ } catch (error: any) {
1038
+ console.error('Error fetching internal provider:', error);
1039
+ return NextResponse.json(
1040
+ { error: 'Failed to fetch provider', details: error.message },
1041
+ { status: 500 }
1042
+ );
1043
+ }
1044
+ }
1045
+ `;
1046
+ }
1047
+ // ─────────────────────────────────────────────────────────
1048
+ // Internal Usage Route — /api/internal/providers/[id]/usage/route.ts
1049
+ // ─────────────────────────────────────────────────────────
1050
+ function generateInternalProviderUsageRoute() {
1051
+ return `// @chimerai component=InternalProviderUsageRoute version=1.0
1052
+ /**
1053
+ * Internal Provider API — Report usage / token consumption
1054
+ * POST /api/internal/providers/[id]/usage
1055
+ *
1056
+ * Called by the Python AI Service after each AI call to track
1057
+ * token usage, costs, and credits in the central ApiUsage table.
1058
+ * Protected by INTERNAL_API_TOKEN (Bearer token).
1059
+ */
1060
+
1061
+ import { NextRequest, NextResponse } from 'next/server';
1062
+ import { prisma } from '@/lib/prisma';
1063
+
1064
+ function validateInternalToken(request: NextRequest): boolean {
1065
+ const authHeader = request.headers.get('authorization') || '';
1066
+ const token = authHeader.replace(/^Bearer\\s+/i, '');
1067
+ const expected = process.env.INTERNAL_API_TOKEN;
1068
+ if (!expected || expected.length < 32) return false;
1069
+ return token === expected;
1070
+ }
1071
+
1072
+ interface UsagePayload {
1073
+ user_id: string;
1074
+ model: string;
1075
+ endpoint: string;
1076
+ prompt_tokens: number;
1077
+ completion_tokens: number;
1078
+ total_tokens?: number;
1079
+ cost?: number;
1080
+ success?: boolean;
1081
+ error_message?: string;
1082
+ response_time?: number;
1083
+ }
1084
+
1085
+ const TOKENS_PER_CREDIT = 1000;
1086
+
1087
+ export async function POST(
1088
+ request: NextRequest,
1089
+ { params }: { params: Promise<{ id: string }> }
1090
+ ) {
1091
+ const { id } = await params;
1092
+ if (!validateInternalToken(request)) {
1093
+ return NextResponse.json({ error: 'Unauthorized' }, { status: 401 });
1094
+ }
1095
+
1096
+ try {
1097
+ const body: UsagePayload = await request.json();
1098
+
1099
+ if (!body.user_id || !body.model || !body.endpoint) {
1100
+ return NextResponse.json(
1101
+ { error: 'Missing required fields: user_id, model, endpoint' },
1102
+ { status: 400 }
1103
+ );
1104
+ }
1105
+
1106
+ const providerId = id;
1107
+ const totalTokens = body.total_tokens ?? (body.prompt_tokens + body.completion_tokens);
1108
+ const creditsUsed = Math.ceil(totalTokens / TOKENS_PER_CREDIT);
1109
+
1110
+ // If cost not provided, try to compute from model pricing
1111
+ let cost = body.cost ?? 0;
1112
+ if (!body.cost && totalTokens > 0) {
1113
+ try {
1114
+ const model = await prisma.model.findFirst({
1115
+ where: { providerId, modelId: body.model },
1116
+ });
1117
+ if (model) {
1118
+ cost =
1119
+ (body.prompt_tokens * model.inputCost) / 1_000_000 +
1120
+ (body.completion_tokens * model.outputCost) / 1_000_000;
1121
+ }
1122
+ } catch {
1123
+ // Pricing lookup failed — use 0
1124
+ }
1125
+ }
1126
+
1127
+ const usage = await prisma.apiUsage.create({
1128
+ data: {
1129
+ userId: body.user_id,
1130
+ providerId,
1131
+ model: body.model,
1132
+ endpoint: body.endpoint,
1133
+ promptTokens: body.prompt_tokens,
1134
+ completionTokens: body.completion_tokens,
1135
+ totalTokens,
1136
+ tokensUsed: totalTokens,
1137
+ creditsUsed,
1138
+ cost,
1139
+ success: body.success ?? true,
1140
+ errorMessage: body.error_message || null,
1141
+ responseTime: body.response_time || 0,
1142
+ },
1143
+ });
1144
+
1145
+ return NextResponse.json({
1146
+ success: true,
1147
+ usage_id: usage.id,
1148
+ credits_used: creditsUsed,
1149
+ cost,
1150
+ });
1151
+ } catch (error: any) {
1152
+ console.error('Error reporting usage:', error);
1153
+ return NextResponse.json(
1154
+ { error: 'Failed to report usage', details: error.message },
1155
+ { status: 500 }
1156
+ );
1157
+ }
1158
+ }
1159
+ `;
1160
+ }
1161
+ // ─────────────────────────────────────────────────────────
1162
+ // Notify Provider Change Utility — lib/notify-provider-change.ts
1163
+ // ─────────────────────────────────────────────────────────
1164
+ function generateNotifyProviderChangeLib() {
1165
+ return `// @chimerai component=NotifyProviderChangeLib version=1.0
1166
+ /**
1167
+ * Notify the AI Service that provider configuration has changed.
1168
+ * Triggers cache invalidation so the service fetches fresh provider data.
1169
+ *
1170
+ * Non-critical: failures are logged but don't throw.
1171
+ */
1172
+
1173
+ export async function notifyProviderChange(): Promise<void> {
1174
+ const aiServiceUrl = process.env.AI_SERVICE_URL || 'http://localhost:8002';
1175
+ const internalToken = process.env.INTERNAL_API_TOKEN;
1176
+
1177
+ if (!internalToken) {
1178
+ console.warn('Cannot notify AI service: INTERNAL_API_TOKEN not configured');
1179
+ return;
1180
+ }
1181
+
1182
+ try {
1183
+ const resp = await fetch(\`\${aiServiceUrl}/api/internal/invalidate-cache\`, {
1184
+ method: 'POST',
1185
+ headers: {
1186
+ Authorization: \`Bearer \${internalToken}\`,
1187
+ 'Content-Type': 'application/json',
1188
+ },
1189
+ signal: AbortSignal.timeout(5000),
1190
+ });
1191
+
1192
+ if (resp.ok) {
1193
+ console.log('AI service provider cache invalidated');
1194
+ } else {
1195
+ console.warn(\`Failed to invalidate AI service cache: \${resp.status}\`);
1196
+ }
1197
+ } catch (error: any) {
1198
+ // Non-critical: AI service might not be running
1199
+ console.debug(\`Could not reach AI service: \${error.message}\`);
1200
+ }
1201
+ }
1202
+ `;
1203
+ }