Files
OpenCode 68a9071cf1 feat: reconcile local master with origin (exa/hot-platform hunters, leaks ledger, latest results)
Local branch had diverged from origin/master (sibling commits on the same base). Rewrote local history linearly on top of origin/master, folding in all local content: exa/hot-platform discovery hunters, leaks ledger, nightly sweep orchestrator, updated .gitignore and skill docs, plus the latest hunt output and vault state. Remote-only files (vault keys, channel scripts) were restored rather than dropped, so the resulting tree is a full union of both sides.
2026-08-07 12:53:47 +08:00

1 line
7.3 KiB
JSON

{"repo": "carsaimz/carsaimz", "files": {"src/app/api/admin/ai-providers/route.ts": {"4d2721e44e3c7ba16136dff6a03b1ec2f0ca17d8": {"t": 1786067565.887701, "n": 7013, "h": "fcbaf1580001837c", "c": "import { NextRequest, NextResponse } from 'next/server'\nimport { safeGetDocs, safeGetDoc, checkFirebaseAdmin } from '@/lib/db-helpers'\nimport { getDocByField, createDoc, updateDoc, deleteDoc } from '@/lib/db'\nimport { serializeFirestore } from '@/lib/serialize'\nimport { invalidateProviderCache } from '@/app/api/chat/route'\nimport { invalidateKnowledgeCache } from '@/lib/chat-knowledge'\n\n/**\n * Carsai Mozambique - AI Providers Admin API\n *\n * CRUD operations for managing AI providers (Groq, DeepSeek, Gemini, OpenRouter, etc.)\n * Admin can add, configure, activate/deactivate, and set priority for failover.\n */\n\n// ── GET: List all providers ──\nexport async function GET() {\n try {\n const adminError = checkFirebaseAdmin()\n if (adminError) {\n return NextResponse.json(\n { success: false, error: adminError },\n { status: 503 }\n )\n }\n\n // Fetch ALL providers and sort client-side — avoids composite index requirement\n const allProviders = await safeGetDocs('ai_providers')\n\n // Sort by priority ascending\n const providers = allProviders.sort((a: any, b: any) => {\n const aPriority = a.priority ?? 99\n const bPriority = b.priority ?? 99\n return aPriority - bPriority\n })\n\n // Mask API keys for security (only show last 4 chars)\n const masked = providers.map(p => ({\n ...serializeFirestore(p),\n apiKey: p.apiKey ? `...${p.apiKey.slice(-4)}` : null,\n }))\n\n return NextResponse.json({ success: true, providers: masked })\n } catch (error) {\n console.error('AI providers GET error:', error)\n return NextResponse.json(\n { success: false, error: 'Failed to fetch providers' },\n { status: 500 }\n )\n }\n}\n\n// ── POST: Create a new provider ──\nexport async function POST(request: NextRequest) {\n try {\n const adminError = checkFirebaseAdmin()\n if (adminError) {\n return NextResponse.json(\n { success: false, error: adminError },\n { status: 503 }\n )\n }\n\n const body = await request.json()\n const { name, displayName, apiKey, baseUrl, model, priority, isActive, config } = body\n\n if (!name) {\n return NextResponse.json({ error: 'Provider name is required' }, { status: 400 })\n }\n\n // Check for duplicate name\n const existing = await getDocByField('ai_providers', 'name', name.toLowerCase())\n if (existing) {\n return NextResponse.json({ error: 'Provider with this name already exists' }, { status: 409 })\n }\n\n // Predefined defaults for common providers\n const defaults: Record<string, { baseUrl: string; model: string; displayName: string }> = {\n groq: {\n baseUrl: 'https://api.groq.com/openai/v1',\n model: 'llama-3.3-70b-versatile',\n displayName: 'Groq (Llama 3.3 70B)',\n },\n deepseek: {\n baseUrl: 'https://api.deepseek.com/v1',\n model: 'deepseek-chat',\n displayName: 'DeepSeek Chat',\n },\n gemini: {\n baseUrl: 'https://generativelanguage.googleapis.com/v1beta/openai',\n model: 'gemini-2.0-flash',\n displayName: 'Google Gemini Flash',\n },\n openrouter: {\n baseUrl: 'https://openrouter.ai/api/v1',\n model: 'openrouter/auto',\n displayName: 'OpenRouter (Auto)',\n },\n openai: {\n baseUrl: 'https://api.openai.com/v1',\n model: 'gpt-4o-mini',\n displayName: 'OpenAI GPT-4o Mini',\n },\n }\n\n const preset = defaults[name.toLowerCase()] || {}\n\n const providerId = await createDoc('ai_providers', {\n name: name.toLowerCase(),\n displayName: displayName || preset.displayName || name,\n apiKey: apiKey || null,\n baseUrl: baseUrl || preset.baseUrl || null,\n model: model || preset.model || null,\n priority: priority ?? 10,\n isActive: isActive ?? true,\n config: config ? JSON.stringify(config) : null,\n })\n\n const provider = await safeGetDoc('ai_providers', providerId)\n\n // Invalidate the chat provider cache so changes take effect immediately\n try { invalidateProviderCache() } catch { /* ignore */ }\n try { invalidateKnowledgeCache() } catch { /* ignore */ }\n\n return NextResponse.json({ success: true, provider: serializeFirestore(provider) })\n } catch (error) {\n console.error('AI providers POST error:', error)\n return NextResponse.json(\n { success: false, error: 'Failed to create provider' },\n { status: 500 }\n )\n }\n}\n\n// ── PUT: Update a provider ──\nexport async function PUT(request: NextRequest) {\n try {\n const adminError = checkFirebaseAdmin()\n if (adminError) {\n return NextResponse.json(\n { success: false, error: adminError },\n { status: 503 }\n )\n }\n\n const body = await request.json()\n const { id, apiKey, baseUrl, model, priority, isActive, displayName, config } = body\n\n if (!id) {\n return NextResponse.json({ error: 'Provider ID is required' }, { status: 400 })\n }\n\n const updateData: Record<string, any> = {}\n if (displayName !== undefined) updateData.displayName = displayName\n if (apiKey !== undefined) updateData.apiKey = apiKey\n if (baseUrl !== undefined) updateData.baseUrl = baseUrl\n if (model !== undefined) updateData.model = model\n if (priority !== undefined) updateData.priority = priority\n if (isActive !== undefined) updateData.isActive = isActive\n if (config !== undefined) updateData.config = config === null ? null : JSON.stringify(config)\n\n await updateDoc('ai_providers', id, updateData)\n const provider = await safeGetDoc('ai_providers', id)\n\n // Invalidate the chat provider cache so changes take effect immediately\n try { invalidateProviderCache() } catch { /* ignore */ }\n try { invalidateKnowledgeCache() } catch { /* ignore */ }\n\n return NextResponse.json({ success: true, provider: serializeFirestore(provider) })\n } catch (error) {\n console.error('AI providers PUT error:', error)\n return NextResponse.json(\n { success: false, error: 'Failed to update provider' },\n { status: 500 }\n )\n }\n}\n\n// ── DELETE: Remove a provider ──\nexport async function DELETE(request: NextRequest) {\n try {\n const adminError = checkFirebaseAdmin()\n if (adminError) {\n return NextResponse.json(\n { success: false, error: adminError },\n { status: 503 }\n )\n }\n\n const { searchParams } = new URL(request.url)\n const id = searchParams.get('id')\n\n if (!id) {\n return NextResponse.json({ error: 'Provider ID is required' }, { status: 400 })\n }\n\n await deleteDoc('ai_providers', id)\n\n // Invalidate the chat provider cache so changes take effect immediately\n try { invalidateProviderCache() } catch { /* ignore */ }\n try { invalidateKnowledgeCache() } catch { /* ignore */ }\n\n return NextResponse.json({ success: true })\n } catch (error) {\n console.error('AI providers DELETE error:', error)\n return NextResponse.json(\n { success: false, error: 'Failed to delete provider' },\n { status: 500 }\n )\n }\n}\n"}}}}