feat: design system overhaul — sidebar, AI chats, settings, brainstorm, color cleanup
All checks were successful
Deploy to Production / Build and Deploy (push) Successful in 12s
All checks were successful
Deploy to Production / Build and Deploy (push) Successful in 12s
- Sidebar: dynamic brand-accent colors, brainstorm section restyled - AI chat general: popup panel with expand/collapse, hides when contextual AI open - AI chat contextual: tabs reordered (Actions first), X close button, height fix - Settings: all tabs restyled, 6 new color presets (sage, terracotta, iron, etc.) - Global color cleanup: emerald/orange hardcoded → brand-accent dynamic - Brainstorm page: orange → brand-accent throughout - PageEntry animation component added to key pages - Floating AI button: bg-brand-accent instead of hardcoded black - i18n: all 15 locales updated with new AI/billing keys - Billing: freemium quota tracking, BYOK, stripe subscription scaffolding - Admin: integrated into new design - AGENTS.md + CLAUDE.md project rules added
This commit is contained in:
106
memento-note/lib/byok/validate-key.ts
Normal file
106
memento-note/lib/byok/validate-key.ts
Normal file
@@ -0,0 +1,106 @@
|
||||
import type { AiGatewayProvider } from '@/lib/ai/router';
|
||||
|
||||
const OPENAI_COMPAT = new Set<string>([
|
||||
'openai',
|
||||
'deepseek',
|
||||
'openrouter',
|
||||
'mistral',
|
||||
'zai',
|
||||
'minimax',
|
||||
'glm',
|
||||
'custom',
|
||||
'lmstudio',
|
||||
]);
|
||||
|
||||
async function validateOpenAiCompatible(
|
||||
apiKey: string,
|
||||
baseUrl: string,
|
||||
): Promise<void> {
|
||||
const res = await fetch(`${baseUrl.replace(/\/$/, '')}/models`, {
|
||||
headers: { Authorization: `Bearer ${apiKey}` },
|
||||
signal: AbortSignal.timeout(10_000),
|
||||
});
|
||||
if (!res.ok) {
|
||||
throw new Error(`Provider rejected the API key (${res.status})`);
|
||||
}
|
||||
}
|
||||
|
||||
async function validateAnthropic(apiKey: string): Promise<void> {
|
||||
const res = await fetch('https://api.anthropic.com/v1/messages', {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'x-api-key': apiKey,
|
||||
'anthropic-version': '2023-06-01',
|
||||
'content-type': 'application/json',
|
||||
},
|
||||
body: JSON.stringify({
|
||||
model: 'claude-3-5-haiku-20241022',
|
||||
max_tokens: 1,
|
||||
messages: [{ role: 'user', content: 'ping' }],
|
||||
}),
|
||||
signal: AbortSignal.timeout(15_000),
|
||||
});
|
||||
if (res.status === 401 || res.status === 403) {
|
||||
throw new Error('Provider rejected the API key');
|
||||
}
|
||||
if (res.status >= 500) {
|
||||
throw new Error('Provider temporarily unavailable; try again');
|
||||
}
|
||||
}
|
||||
|
||||
async function validateGoogle(apiKey: string): Promise<void> {
|
||||
const res = await fetch(
|
||||
`https://generativelanguage.googleapis.com/v1beta/models?key=${encodeURIComponent(apiKey)}`,
|
||||
{ signal: AbortSignal.timeout(10_000) },
|
||||
);
|
||||
if (!res.ok) {
|
||||
throw new Error(`Provider rejected the API key (${res.status})`);
|
||||
}
|
||||
}
|
||||
|
||||
const BASE_URLS: Partial<Record<AiGatewayProvider, string>> = {
|
||||
deepseek: 'https://api.deepseek.com/v1',
|
||||
openrouter: 'https://openrouter.ai/api/v1',
|
||||
mistral: 'https://api.mistral.ai/v1',
|
||||
zai: 'https://api.zukijourney.com/v1',
|
||||
minimax: 'https://api.minimax.io/v1',
|
||||
glm: 'https://open.bigmodel.ai/api/paas/v4',
|
||||
openai: 'https://api.openai.com/v1',
|
||||
};
|
||||
|
||||
export async function validateProviderApiKey(
|
||||
provider: AiGatewayProvider,
|
||||
apiKey: string,
|
||||
baseUrl?: string,
|
||||
): Promise<void> {
|
||||
if (!apiKey.trim()) {
|
||||
throw new Error('API key is required');
|
||||
}
|
||||
|
||||
if (provider === 'anthropic' || provider === 'anthropic_custom') {
|
||||
await validateAnthropic(apiKey);
|
||||
return;
|
||||
}
|
||||
|
||||
if (provider === 'google') {
|
||||
await validateGoogle(apiKey);
|
||||
return;
|
||||
}
|
||||
|
||||
if (provider === 'ollama' || provider === 'lmstudio') {
|
||||
throw new Error('Local providers are not supported for BYOK');
|
||||
}
|
||||
|
||||
if (OPENAI_COMPAT.has(provider)) {
|
||||
const url = provider === 'custom'
|
||||
? baseUrl
|
||||
: BASE_URLS[provider as AiGatewayProvider];
|
||||
if (!url) {
|
||||
throw new Error('Base URL is required for this provider');
|
||||
}
|
||||
await validateOpenAiCompatible(apiKey, url);
|
||||
return;
|
||||
}
|
||||
|
||||
throw new Error(`Unsupported provider: ${provider}`);
|
||||
}
|
||||
Reference in New Issue
Block a user