Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -26,3 +26,5 @@ tsconfig.tsbuildinfo
.eval-run-id

dist

docker-compose.override.yml
2 changes: 1 addition & 1 deletion apps/dbagent/next-env.d.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
/// <reference types="next" />
/// <reference types="next/image-types/global" />
import type {} from './.next/types/routes.d.ts';
/// <reference path="./.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.
7 changes: 6 additions & 1 deletion apps/dbagent/src/app/api/mcp/servers/route.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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(
Expand Down
2 changes: 1 addition & 1 deletion apps/dbagent/src/app/layout.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@ export const metadata: Metadata = {
export default async function RootLayout({ children }: { children: React.ReactNode }) {
return (
<html suppressHydrationWarning lang="en" className={`${GeistSans.className} ${geistMono.variable}`}>
<body>
<body suppressHydrationWarning>
<Providers>{children}</Providers>
</body>
</html>
Expand Down
3 changes: 0 additions & 3 deletions apps/dbagent/src/components/playbooks/action.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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
});

Expand Down
305 changes: 155 additions & 150 deletions apps/dbagent/src/lib/ai/providers/builtin.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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<ProviderRegistry> {
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<ProviderRegistry> {
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<string, Model> = (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<ProviderRegistry> {
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<ProviderRegistry> {
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<ProviderRegistry> {
const providers: Array<[string, () => Promise<ProviderRegistry>]> = [];

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<string, string> = {
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<string, string> = {
chat: defaultModel.info().id,
title: smallModel.info().id,
summary: smallModel.info().id
};

return createRegistryFromModels({ models: allModels, aliases, defaultModel });
}
2 changes: 1 addition & 1 deletion apps/dbagent/src/lib/ai/providers/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,7 @@ function buildProviderRegistry() {
})
);
} else {
registries.push(() => Promise.resolve(getBuiltinProviderRegistry()));
registries.push(getBuiltinProviderRegistry);
}

// Add optional registries.
Expand Down