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/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/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/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 }); 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.