Files
Momento/memento-note/lib/ai/factory.ts
Antigravity a623454347
Some checks failed
CI / Lint, Unit Tests & Build (push) Failing after 1m32s
CI / Deploy production (on server) (push) Has been skipped
perf: memo GridCard, fuse save fns, fix slash tab active color
2026-06-14 14:06:05 +00:00

286 lines
13 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
import { OpenAIProvider } from './providers/openai';
import { OllamaProvider } from './providers/ollama';
import { CustomOpenAIProvider } from './providers/custom-openai';
import { AnthropicProvider } from './providers/anthropic';
import { GoogleProvider } from './providers/google';
import { AIProvider } from './types';
import { resolveAiRoute } from './router';
export type ProviderType =
| 'ollama'
| 'openai'
| 'google'
| 'minimax'
| 'glm'
| 'custom'
| 'deepseek'
| 'openrouter'
| 'mistral'
| 'zai'
| 'lmstudio'
| 'anthropic'
| 'anthropic_custom'
| 'custom_openai'
| 'custom_anthropic';
// --- Provider defaults ---
const PROVIDER_DEFAULTS: Record<string, { baseUrl: string; model: string; embeddingModel: string }> = {
deepseek: {
baseUrl: 'https://api.deepseek.com/v1',
model: 'deepseek-chat',
embeddingModel: '',
},
openrouter: {
baseUrl: 'https://openrouter.ai/api/v1',
model: 'openai/gpt-4o-mini',
embeddingModel: 'openai/text-embedding-3-small',
},
mistral: {
baseUrl: 'https://api.mistral.ai/v1',
model: 'mistral-small-latest',
embeddingModel: 'mistral-embed',
},
zai: {
baseUrl: 'https://api.zukijourney.com/v1',
model: 'gpt-4o-mini',
embeddingModel: 'text-embedding-3-small',
},
lmstudio: {
baseUrl: 'http://localhost:1234/v1',
model: '',
embeddingModel: '',
},
google: {
baseUrl: '',
model: 'gemini-1.5-flash',
embeddingModel: 'text-embedding-004',
},
minimax: {
baseUrl: 'https://api.minimax.io/v1',
model: 'abab6.5-chat',
embeddingModel: '',
},
glm: {
baseUrl: 'https://open.bigmodel.ai/api/paas/v4',
model: 'glm-4',
embeddingModel: 'embedding-2',
},
};
function createOllamaProvider(config: Record<string, string>, modelName: string, embeddingModelName: string, baseUrlOverride?: string): OllamaProvider {
let baseUrl = baseUrlOverride || config?.OLLAMA_BASE_URL || process.env.OLLAMA_BASE_URL
// Only use localhost as fallback for local development (not in Docker)
if (!baseUrl && process.env.NODE_ENV !== 'production') {
baseUrl = 'http://localhost:11434'
}
if (!baseUrl) {
throw new Error('OLLAMA_BASE_URL is required when using Ollama provider')
}
// Ensure baseUrl doesn't end with /api, we'll add it in OllamaProvider
if (baseUrl.endsWith('/api')) {
baseUrl = baseUrl.slice(0, -4);
}
return new OllamaProvider(baseUrl, modelName, embeddingModelName);
}
function createOpenAIProvider(config: Record<string, string>, modelName: string, embeddingModelName: string): OpenAIProvider {
const apiKey = config?.OPENAI_API_KEY || process.env.OPENAI_API_KEY || '';
if (!apiKey) {
throw new Error('OPENAI_API_KEY is required when using OpenAI provider');
}
return new OpenAIProvider(apiKey, modelName, embeddingModelName);
}
function createCustomOpenAIProvider(config: Record<string, string>, modelName: string, embeddingModelName: string): CustomOpenAIProvider {
const apiKey = config?.CUSTOM_OPENAI_API_KEY || process.env.CUSTOM_OPENAI_API_KEY || '';
const baseUrl = config?.CUSTOM_OPENAI_BASE_URL || process.env.CUSTOM_OPENAI_BASE_URL || '';
if (!apiKey) {
throw new Error('CUSTOM_OPENAI_API_KEY is required when using Custom OpenAI provider');
}
if (!baseUrl) {
throw new Error('CUSTOM_OPENAI_BASE_URL is required when using Custom OpenAI provider');
}
return new CustomOpenAIProvider(apiKey, baseUrl, modelName, embeddingModelName);
}
function createDeepSeekProvider(config: Record<string, string>, modelName: string, embeddingModelName: string): CustomOpenAIProvider {
const apiKey = config?.DEEPSEEK_API_KEY || process.env.DEEPSEEK_API_KEY || '';
if (!apiKey) throw new Error('DEEPSEEK_API_KEY is required when using DeepSeek provider');
const defaults = PROVIDER_DEFAULTS.deepseek;
return new CustomOpenAIProvider(apiKey, defaults.baseUrl, modelName || defaults.model, embeddingModelName || defaults.embeddingModel);
}
function createOpenRouterProvider(config: Record<string, string>, modelName: string, embeddingModelName: string): CustomOpenAIProvider {
const apiKey = config?.OPENROUTER_API_KEY || process.env.OPENROUTER_API_KEY || config?.CUSTOM_OPENAI_API_KEY || process.env.CUSTOM_OPENAI_API_KEY || '';
if (!apiKey) throw new Error('OPENROUTER_API_KEY is required when using OpenRouter provider');
const defaults = PROVIDER_DEFAULTS.openrouter;
return new CustomOpenAIProvider(apiKey, defaults.baseUrl, modelName || defaults.model, embeddingModelName || defaults.embeddingModel);
}
function createMistralProvider(config: Record<string, string>, modelName: string, embeddingModelName: string): CustomOpenAIProvider {
const apiKey = config?.MISTRAL_API_KEY || process.env.MISTRAL_API_KEY || '';
if (!apiKey) throw new Error('MISTRAL_API_KEY is required when using Mistral provider');
const defaults = PROVIDER_DEFAULTS.mistral;
return new CustomOpenAIProvider(apiKey, defaults.baseUrl, modelName || defaults.model, embeddingModelName || defaults.embeddingModel);
}
function createZAIProvider(config: Record<string, string>, modelName: string, embeddingModelName: string): CustomOpenAIProvider {
const apiKey = config?.ZAI_API_KEY || process.env.ZAI_API_KEY || '';
if (!apiKey) throw new Error('ZAI_API_KEY is required when using Z.AI provider');
const defaults = PROVIDER_DEFAULTS.zai;
return new CustomOpenAIProvider(apiKey, defaults.baseUrl, modelName || defaults.model, embeddingModelName || defaults.embeddingModel);
}
function createLMStudioProvider(config: Record<string, string>, modelName: string, embeddingModelName: string): CustomOpenAIProvider {
const baseUrl = config?.LMSTUDIO_BASE_URL || process.env.LMSTUDIO_BASE_URL || PROVIDER_DEFAULTS.lmstudio.baseUrl;
// LM Studio doesn't require an API key, but the CustomOpenAI provider needs one
// Use a dummy key if not provided
const apiKey = config?.LMSTUDIO_API_KEY || process.env.LMSTUDIO_API_KEY || 'lm-studio';
return new CustomOpenAIProvider(apiKey, baseUrl, modelName, embeddingModelName);
}
function createAnthropicProvider(config: Record<string, string>, modelName: string): AnthropicProvider {
const apiKey = config?.ANTHROPIC_API_KEY || process.env.ANTHROPIC_API_KEY || '';
if (!apiKey) {
throw new Error('ANTHROPIC_API_KEY is required when using Anthropic provider');
}
return new AnthropicProvider(apiKey, modelName || 'claude-sonnet-4-20250514');
}
/**
* Passerelles compatibles **Anthropic Messages API** (ex. MiniMax), pas OpenAI.
* Le SDK envoie les requêtes vers `{baseURL}/messages` avec len-tête `x-api-key`.
*/
function createAnthropicCustomProvider(config: Record<string, string>, modelName: string): AnthropicProvider {
const apiKey = config?.ANTHROPIC_CUSTOM_API_KEY || process.env.ANTHROPIC_CUSTOM_API_KEY || '';
const baseUrl = config?.ANTHROPIC_CUSTOM_BASE_URL || process.env.ANTHROPIC_CUSTOM_BASE_URL || '';
if (!apiKey) {
throw new Error('ANTHROPIC_CUSTOM_API_KEY is required when using Anthropic Custom provider');
}
if (!baseUrl) {
throw new Error('ANTHROPIC_CUSTOM_BASE_URL is required when using Anthropic Custom provider');
}
const resolvedModel =
modelName && modelName.trim() !== '' ? modelName.trim() : 'MiniMax-M2.7';
return new AnthropicProvider(apiKey, resolvedModel, baseUrl.trim());
}
function createGoogleProvider(config: Record<string, string>, modelName: string, embeddingModelName: string): GoogleProvider {
const apiKey = config?.GOOGLE_GENERATIVE_AI_API_KEY || process.env.GOOGLE_GENERATIVE_AI_API_KEY || '';
if (!apiKey) throw new Error('GOOGLE_GENERATIVE_AI_API_KEY is required when using Google provider');
return new GoogleProvider(apiKey, modelName || PROVIDER_DEFAULTS.google.model, embeddingModelName || PROVIDER_DEFAULTS.google.embeddingModel);
}
function createMiniMaxProvider(config: Record<string, string>, modelName: string, embeddingModelName: string): CustomOpenAIProvider {
const apiKey = config?.MINIMAX_API_KEY || process.env.MINIMAX_API_KEY || '';
if (!apiKey) throw new Error('MINIMAX_API_KEY is required when using MiniMax provider');
const defaults = PROVIDER_DEFAULTS.minimax;
return new CustomOpenAIProvider(apiKey, defaults.baseUrl, modelName || defaults.model, embeddingModelName || defaults.embeddingModel);
}
function createGLMProvider(config: Record<string, string>, modelName: string, embeddingModelName: string): CustomOpenAIProvider {
const apiKey = config?.GLM_API_KEY || process.env.GLM_API_KEY || '';
if (!apiKey) throw new Error('GLM_API_KEY is required when using GLM provider');
const defaults = PROVIDER_DEFAULTS.glm;
return new CustomOpenAIProvider(apiKey, defaults.baseUrl, modelName || defaults.model, embeddingModelName || defaults.embeddingModel);
}
/** Exported for tests and Story 3.3+ orchestration; prefer `get*Provider` in production call sites. */
export function getProviderInstance(providerType: ProviderType, config: Record<string, string>, modelName: string, embeddingModelName: string, ollamaBaseUrl?: string): AIProvider {
switch (providerType) {
case 'ollama':
return createOllamaProvider(config, modelName, embeddingModelName, ollamaBaseUrl);
case 'openai':
return createOpenAIProvider(config, modelName, embeddingModelName);
case 'custom':
case 'custom_openai':
return createCustomOpenAIProvider(config, modelName, embeddingModelName);
case 'deepseek':
return createDeepSeekProvider(config, modelName, embeddingModelName);
case 'openrouter':
return createOpenRouterProvider(config, modelName, embeddingModelName);
case 'mistral':
return createMistralProvider(config, modelName, embeddingModelName);
case 'zai':
return createZAIProvider(config, modelName, embeddingModelName);
case 'lmstudio':
return createLMStudioProvider(config, modelName, embeddingModelName);
case 'anthropic':
return createAnthropicProvider(config, modelName);
case 'anthropic_custom':
case 'custom_anthropic':
return createAnthropicCustomProvider(config, modelName);
case 'google':
return createGoogleProvider(config, modelName, embeddingModelName);
case 'minimax':
return createMiniMaxProvider(config, modelName, embeddingModelName);
case 'glm':
return createGLMProvider(config, modelName, embeddingModelName);
default:
return createOllamaProvider(config, modelName, embeddingModelName, ollamaBaseUrl);
}
}
// Resolve the effective provider type and config keys for a given provider
// Returns { providerType, apiKeyConfigKey, baseUrlConfigKey }
function getProviderConfigKeys(providerType: string): { apiKeyConfigKey: string; baseUrlConfigKey: string } {
switch (providerType) {
case 'deepseek': return { apiKeyConfigKey: 'DEEPSEEK_API_KEY', baseUrlConfigKey: '' };
case 'openrouter': return { apiKeyConfigKey: 'OPENROUTER_API_KEY', baseUrlConfigKey: '' };
case 'mistral': return { apiKeyConfigKey: 'MISTRAL_API_KEY', baseUrlConfigKey: '' };
case 'zai': return { apiKeyConfigKey: 'ZAI_API_KEY', baseUrlConfigKey: '' };
case 'lmstudio': return { apiKeyConfigKey: 'LMSTUDIO_API_KEY', baseUrlConfigKey: 'LMSTUDIO_BASE_URL' };
case 'openai': return { apiKeyConfigKey: 'OPENAI_API_KEY', baseUrlConfigKey: '' };
case 'anthropic': return { apiKeyConfigKey: 'ANTHROPIC_API_KEY', baseUrlConfigKey: '' };
case 'anthropic_custom':
return { apiKeyConfigKey: 'ANTHROPIC_CUSTOM_API_KEY', baseUrlConfigKey: 'ANTHROPIC_CUSTOM_BASE_URL' };
case 'google': return { apiKeyConfigKey: 'GOOGLE_GENERATIVE_AI_API_KEY', baseUrlConfigKey: '' };
case 'minimax': return { apiKeyConfigKey: 'MINIMAX_API_KEY', baseUrlConfigKey: '' };
case 'glm': return { apiKeyConfigKey: 'GLM_API_KEY', baseUrlConfigKey: '' };
case 'custom':
case 'custom_openai':
return { apiKeyConfigKey: 'CUSTOM_OPENAI_API_KEY', baseUrlConfigKey: 'CUSTOM_OPENAI_BASE_URL' };
case 'custom_anthropic':
return { apiKeyConfigKey: 'ANTHROPIC_CUSTOM_API_KEY', baseUrlConfigKey: 'ANTHROPIC_CUSTOM_BASE_URL' };
default: return { apiKeyConfigKey: '', baseUrlConfigKey: 'OLLAMA_BASE_URL' };
}
}
export function getTagsProvider(config?: Record<string, string>): AIProvider {
const cfg = config || {};
const route = resolveAiRoute('tags', cfg);
return getProviderInstance(route.providerType as ProviderType, cfg, route.modelName, route.embeddingModelName, route.ollamaBaseUrl);
}
export function getEmbeddingsProvider(config?: Record<string, string>): AIProvider {
const cfg = config || {};
const route = resolveAiRoute('embedding', cfg);
return getProviderInstance(route.providerType as ProviderType, cfg, route.modelName, route.embeddingModelName, route.ollamaBaseUrl);
}
export function getAIProvider(config?: Record<string, string>): AIProvider {
return getEmbeddingsProvider(config);
}
export function getChatProvider(config?: Record<string, string>): AIProvider {
const cfg = config || {};
const route = resolveAiRoute('chat', cfg);
return getProviderInstance(route.providerType as ProviderType, cfg, route.modelName, route.embeddingModelName, route.ollamaBaseUrl);
}
// Export for use by admin settings form and deploy scripts
export { getProviderConfigKeys };