From ba89c478929635b71413d069a52485e514f4201b Mon Sep 17 00:00:00 2001 From: Warda Bibi Date: Mon, 6 Apr 2026 16:27:23 +0500 Subject: [PATCH 1/2] feat: dynamic AI model registry fetched from provider APIs Replace hardcoded model lists with live fetches from OpenAI, Anthropic, Google, and DeepSeek APIs at startup. Providers load in parallel via Promise.allSettled so one failure does not block the others. Aliases (chat/title/summary) are resolved dynamically using a small-model heuristic. Also fix React hydration warning on and update next-env.d.ts reference syntax. --- .gitignore | 2 + apps/dbagent/next-env.d.ts | 2 +- apps/dbagent/src/app/layout.tsx | 2 +- apps/dbagent/src/lib/ai/providers/builtin.ts | 305 ++++++++++--------- apps/dbagent/src/lib/ai/providers/index.ts | 2 +- 5 files changed, 160 insertions(+), 153 deletions(-) diff --git a/.gitignore b/.gitignore index fd79d5b3..d2dfbaa6 100644 --- a/.gitignore +++ b/.gitignore @@ -26,3 +26,5 @@ tsconfig.tsbuildinfo .eval-run-id dist + +docker-compose.override.yml diff --git a/apps/dbagent/next-env.d.ts b/apps/dbagent/next-env.d.ts index 6eb4e682..830fb594 100644 --- a/apps/dbagent/next-env.d.ts +++ b/apps/dbagent/next-env.d.ts @@ -1,6 +1,6 @@ /// /// -import type {} from './.next/types/routes.d.ts'; +/// // NOTE: This file should not be edited // see https://nextjs.org/docs/app/api-reference/config/typescript for more information. diff --git a/apps/dbagent/src/app/layout.tsx b/apps/dbagent/src/app/layout.tsx index f3a2e205..c9b09f82 100644 --- a/apps/dbagent/src/app/layout.tsx +++ b/apps/dbagent/src/app/layout.tsx @@ -17,7 +17,7 @@ export const metadata: Metadata = { export default async function RootLayout({ children }: { children: React.ReactNode }) { return ( - + {children} diff --git a/apps/dbagent/src/lib/ai/providers/builtin.ts b/apps/dbagent/src/lib/ai/providers/builtin.ts index b499d1dc..3d833f22 100644 --- a/apps/dbagent/src/lib/ai/providers/builtin.ts +++ b/apps/dbagent/src/lib/ai/providers/builtin.ts @@ -4,160 +4,165 @@ import { google } from '@ai-sdk/google'; import { createOpenAI } from '@ai-sdk/openai'; import { env } from '~/lib/env/server'; -import { Model, Provider, ProviderModel, ProviderRegistry } from './types'; -import { createModel, createRegistryFromModels } from './utils'; - -type BuiltinProvider = Provider & { - models: BuiltinProviderModel[]; -}; - -type BuiltinProviderModel = ProviderModel & { - providerId: string; -}; - -const openai = createOpenAI({ - baseURL: env.OPENAI_BASE_URL, - apiKey: env.OPENAI_API_KEY -}); - -const builtinOpenAIModels: BuiltinProvider = { - info: { - name: 'OpenAI', - id: 'openai', - kind: openai, - fallback: 'gpt-5' - }, - models: [ - { - id: 'openai:gpt-5', - providerId: 'gpt-5', - name: 'GPT-5' - }, - { - id: 'openai:gpt-5-turbo', - providerId: 'gpt-5-turbo', - name: 'GPT-5 Turbo' - }, - { - id: 'openai:gpt-5-mini', - providerId: 'gpt-5-mini', - name: 'GPT-5 Mini' - }, - { - id: 'openai:gpt-4o', - providerId: 'gpt-4o', - name: 'GPT-4o' - } - ] -}; - -const builtinDeepseekModels: BuiltinProvider = { - info: { - name: 'DeepSeek', - id: 'deepseek', - kind: deepseek - }, - models: [ - { - id: 'deepseek:chat', - providerId: 'deepseek-chat', - name: 'DeepSeek Chat' - } - ] -}; - -const builtinAnthropicModels: BuiltinProvider = { - info: { - name: 'Anthropic', - id: 'anthropic', - kind: anthropic - }, - models: [ - { - id: 'anthropic:claude-sonnet-4-5', - providerId: 'claude-sonnet-4-5', - name: 'Claude Sonnet 4.5' - }, - { - id: 'anthropic:claude-opus-4-1', - providerId: 'claude-opus-4-1', - name: 'Claude Opus 4.1' - } - ] -}; - -const builtinGoogleModels: BuiltinProvider = { - info: { - name: 'Google', - id: 'google', - kind: google - }, - models: [ - { - id: 'google:gemini-2.5-pro', - providerId: 'gemini-2.5-pro', - name: 'Gemini 2.5 Pro' - }, - { - id: 'google:gemini-2.5-flash', - providerId: 'gemini-2.5-flash', - name: 'Gemini 2.5 Flash' - }, - { - id: 'google:gemini-2.5-flash-lite', - providerId: 'gemini-2.5-flash-lite', - name: 'Gemini 2.5 Flash Lite' +import { Model, ProviderRegistry } from './types'; +import { combineRegistries, createModel, createRegistryFromModels } from './utils'; + +// ─── OpenAI ────────────────────────────────────────────────────────────────── + +const OPENAI_EXCLUDED_KEYWORDS = [ + 'embedding', + 'whisper', + 'tts', + 'dall-e', + 'moderation', + 'realtime', + 'audio', + 'instruct' +]; +const OPENAI_CHAT_PATTERN = /^(gpt-|o\d|chatgpt-)/; + +function isOpenAIChatModel(id: string): boolean { + const lower = id.toLowerCase(); + if (OPENAI_EXCLUDED_KEYWORDS.some((kw) => lower.includes(kw))) return false; + return OPENAI_CHAT_PATTERN.test(lower); +} + +async function createOpenAIRegistry(): Promise { + const openaiProvider = createOpenAI({ baseURL: env.OPENAI_BASE_URL, apiKey: env.OPENAI_API_KEY }); + const baseUrl = (env.OPENAI_BASE_URL ?? 'https://api.openai.com').replace(/\/$/, ''); + + const response = await fetch(`${baseUrl}/v1/models`, { + headers: { Authorization: `Bearer ${env.OPENAI_API_KEY}` } + }); + if (!response.ok) throw new Error(`OpenAI models API error: ${response.status} ${response.statusText}`); + + const data = (await response.json()) as { data: Array<{ id: string }> }; + const models: Model[] = data.data + .filter((m) => isOpenAIChatModel(m.id)) + .sort((a, b) => a.id.localeCompare(b.id)) + .map((m) => createModel({ id: `openai:${m.id}`, name: m.id }, () => openaiProvider.languageModel(m.id))); + + if (models.length === 0) throw new Error('OpenAI returned no usable chat models'); + return createRegistryFromModels({ models }); +} + +// ─── Anthropic ─────────────────────────────────────────────────────────────── + +async function createAnthropicRegistry(): Promise { + const response = await fetch('https://api.anthropic.com/v1/models', { + headers: { + 'x-api-key': env.ANTHROPIC_API_KEY as string, + 'anthropic-version': '2023-06-01' } - ] -}; + }); + if (!response.ok) throw new Error(`Anthropic models API error: ${response.status} ${response.statusText}`); -const builtinProviderModels: Record = (function () { - const activeList: BuiltinProvider[] = []; - if (env.OPENAI_API_KEY) { - activeList.push(builtinOpenAIModels); - } - if (env.DEEPSEEK_API_KEY) { - activeList.push(builtinDeepseekModels); - } - if (env.ANTHROPIC_API_KEY) { - activeList.push(builtinAnthropicModels); + const data = (await response.json()) as { data: Array<{ id: string; display_name: string }> }; + const models: Model[] = data.data.map((m) => + createModel({ id: `anthropic:${m.id}`, name: m.display_name }, () => anthropic.languageModel(m.id)) + ); + + if (models.length === 0) throw new Error('Anthropic returned no usable models'); + return createRegistryFromModels({ models }); +} + +// ─── Google ────────────────────────────────────────────────────────────────── + +async function createGoogleRegistry(): Promise { + const response = await fetch( + `https://generativelanguage.googleapis.com/v1beta/models?key=${env.GOOGLE_GENERATIVE_AI_API_KEY}` + ); + if (!response.ok) throw new Error(`Google models API error: ${response.status} ${response.statusText}`); + + const data = (await response.json()) as { + models: Array<{ name: string; displayName: string; supportedGenerationMethods: string[] }>; + }; + const models: Model[] = data.models + .filter((m) => m.supportedGenerationMethods.includes('generateContent')) + .map((m) => { + const modelId = m.name.replace('models/', ''); + return createModel({ id: `google:${modelId}`, name: m.displayName }, () => google.languageModel(modelId)); + }); + + if (models.length === 0) throw new Error('Google returned no usable chat models'); + return createRegistryFromModels({ models }); +} + +// ─── DeepSeek ──────────────────────────────────────────────────────────────── + +async function createDeepSeekRegistry(): Promise { + const response = await fetch('https://api.deepseek.com/models', { + headers: { Authorization: `Bearer ${env.DEEPSEEK_API_KEY}` } + }); + if (!response.ok) throw new Error(`DeepSeek models API error: ${response.status} ${response.statusText}`); + + const data = (await response.json()) as { data: Array<{ id: string }> }; + const models: Model[] = data.data.map((m) => + createModel({ id: `deepseek:${m.id}`, name: m.id }, () => deepseek.languageModel(m.id)) + ); + + if (models.length === 0) throw new Error('DeepSeek returned no usable models'); + return createRegistryFromModels({ models }); +} + +// ─── Aliases ───────────────────────────────────────────────────────────────── + +// Patterns that indicate a lightweight/cheap model suitable for title/summary tasks +const SMALL_MODEL_PATTERNS = ['mini', 'flash', 'haiku', 'lite', 'nano']; + +function findSmallModel(models: Model[]): Model | undefined { + return models.find((m) => { + const lower = m.info().id.toLowerCase(); + return SMALL_MODEL_PATTERNS.some((p) => lower.includes(p)); + }); +} + +// ─── Combined builtin registry ─────────────────────────────────────────────── + +export async function getBuiltinProviderRegistry(): Promise { + const providers: Array<[string, () => Promise]> = []; + + if (env.OPENAI_API_KEY) providers.push(['OpenAI', createOpenAIRegistry]); + if (env.ANTHROPIC_API_KEY) providers.push(['Anthropic', createAnthropicRegistry]); + if (env.GOOGLE_GENERATIVE_AI_API_KEY) providers.push(['Google', createGoogleRegistry]); + if (env.DEEPSEEK_API_KEY) providers.push(['DeepSeek', createDeepSeekRegistry]); + + if (providers.length === 0) { + throw new Error('No providers enabled. Please configure at least one API key'); } - if (env.GOOGLE_GENERATIVE_AI_API_KEY) { - activeList.push(builtinGoogleModels); + + const results = await Promise.allSettled(providers.map(([, fn]) => fn())); + + const registries: ProviderRegistry[] = []; + for (let i = 0; i < results.length; i++) { + const result = results[i]!; + if (result.status === 'fulfilled') { + registries.push(result.value); + } else { + console.warn(`[builtin] ${providers[i]![0]} registry failed:`, result.reason); + } } - if (activeList.length === 0) { - throw new Error('No providers enabled. Please configure API keys'); + if (registries.length === 0) { + const errors = results + .filter((r): r is PromiseRejectedResult => r.status === 'rejected') + .map((r) => String(r.reason)) + .join('; '); + throw new Error(`All provider registries failed to load: ${errors}`); } - return Object.fromEntries( - activeList.flatMap((p) => { - const factory = p.info.kind; - return p.models.map((model: BuiltinProviderModel) => { - const modelInstance = createModel(model, () => factory.languageModel(model.providerId)); - return [modelInstance.info().id, modelInstance]; - }); - }) - ); -})(); - -// We default to OpenAI GPT-5 if available, otherwise fallback to the first model in the list -const fallbackModel = Object.values(builtinProviderModels)[0]!; -const defaultLanguageModel = builtinProviderModels['openai:gpt-5'] ?? fallbackModel; -const defaultTitleModel = builtinProviderModels['openai:gpt-5-mini'] ?? fallbackModel; -const defaultSummaryModel = builtinProviderModels['openai:gpt-5-mini'] ?? fallbackModel; - -const builtinModelAliases: Record = { - chat: defaultLanguageModel.info().id, - title: defaultTitleModel.info().id, - summary: defaultSummaryModel.info().id -}; - -const builtinProviderRegistry = createRegistryFromModels({ - models: builtinProviderModels, - aliases: builtinModelAliases, - defaultModel: defaultLanguageModel -}); - -export function getBuiltinProviderRegistry(): ProviderRegistry { - return builtinProviderRegistry; + + // Combine all provider registries into one, then build aliases over the full model list + const combined = combineRegistries(registries); + const allModels = combined.listLanguageModels(); + + const defaultModel = allModels[0]!; + const smallModel = findSmallModel(allModels) ?? defaultModel; + + const aliases: Record = { + chat: defaultModel.info().id, + title: smallModel.info().id, + summary: smallModel.info().id + }; + + return createRegistryFromModels({ models: allModels, aliases, defaultModel }); } diff --git a/apps/dbagent/src/lib/ai/providers/index.ts b/apps/dbagent/src/lib/ai/providers/index.ts index 12f62a34..3dbeb163 100644 --- a/apps/dbagent/src/lib/ai/providers/index.ts +++ b/apps/dbagent/src/lib/ai/providers/index.ts @@ -29,7 +29,7 @@ function buildProviderRegistry() { }) ); } else { - registries.push(() => Promise.resolve(getBuiltinProviderRegistry())); + registries.push(getBuiltinProviderRegistry); } // Add optional registries. From f04edad7ccea7b82086a94a6619c8802e3d4a0ec Mon Sep 17 00:00:00 2001 From: Warda Bibi Date: Tue, 7 Apr 2026 22:19:45 +0500 Subject: [PATCH 2/2] fix: handle missing mcp-source/dist dir and remove conflicting topP param --- apps/dbagent/src/app/api/mcp/servers/route.ts | 7 ++++++- apps/dbagent/src/components/playbooks/action.ts | 3 --- 2 files changed, 6 insertions(+), 4 deletions(-) diff --git a/apps/dbagent/src/app/api/mcp/servers/route.ts b/apps/dbagent/src/app/api/mcp/servers/route.ts index e98c9988..4a561039 100644 --- a/apps/dbagent/src/app/api/mcp/servers/route.ts +++ b/apps/dbagent/src/app/api/mcp/servers/route.ts @@ -6,7 +6,12 @@ const mcpServersDir = getMCPServersDir(); export async function GET() { try { - const files = await fs.readdir(mcpServersDir); + let files: string[]; + try { + files = await fs.readdir(mcpServersDir); + } catch { + return NextResponse.json([]); + } const serverFiles = files.filter((file) => file.endsWith('.js')); const servers = await Promise.all( diff --git a/apps/dbagent/src/components/playbooks/action.ts b/apps/dbagent/src/components/playbooks/action.ts index 03e2e9f8..bec92ef6 100644 --- a/apps/dbagent/src/components/playbooks/action.ts +++ b/apps/dbagent/src/components/playbooks/action.ts @@ -49,10 +49,7 @@ export async function actionGeneratePlaybookContent(name: string, description: s content: prompt } ], - //lower values for temperature and topP are more deterministic higher are more creative(max 2.0) - //0.1 is deterministic, 0.7 is creative temperature: 0.2, - topP: 0.1, maxTokens: 1000 });