glass/src/common/services/modelStateService.js

324 lines
12 KiB
JavaScript

const Store = require('electron-store');
const fetch = require('node-fetch');
const { ipcMain, webContents } = require('electron');
const { PROVIDERS } = require('../ai/factory');
class ModelStateService {
constructor(authService) {
this.authService = authService;
this.store = new Store({ name: 'pickle-glass-model-state' });
this.state = {};
}
initialize() {
this._loadStateForCurrentUser();
this.setupIpcHandlers();
console.log('[ModelStateService] Initialized.');
}
_logCurrentSelection() {
const llmModel = this.state.selectedModels.llm;
const sttModel = this.state.selectedModels.stt;
const llmProvider = this.getProviderForModel('llm', llmModel) || 'None';
const sttProvider = this.getProviderForModel('stt', sttModel) || 'None';
console.log(`[ModelStateService] 🌟 Current Selection -> LLM: ${llmModel || 'None'} (Provider: ${llmProvider}), STT: ${sttModel || 'None'} (Provider: ${sttProvider})`);
}
_autoSelectAvailableModels() {
console.log('[ModelStateService] Running auto-selection for models...');
const types = ['llm', 'stt'];
types.forEach(type => {
const currentModelId = this.state.selectedModels[type];
let isCurrentModelValid = false;
if (currentModelId) {
const provider = this.getProviderForModel(type, currentModelId);
if (provider && this.getApiKey(provider)) {
isCurrentModelValid = true;
}
}
if (!isCurrentModelValid) {
console.log(`[ModelStateService] No valid ${type.toUpperCase()} model selected. Finding an alternative...`);
const availableModels = this.getAvailableModels(type);
if (availableModels.length > 0) {
this.state.selectedModels[type] = availableModels[0].id;
console.log(`[ModelStateService] Auto-selected ${type.toUpperCase()} model: ${availableModels[0].id}`);
} else {
this.state.selectedModels[type] = null;
}
}
});
}
_loadStateForCurrentUser() {
const userId = this.authService.getCurrentUserId();
const initialApiKeys = Object.keys(PROVIDERS).reduce((acc, key) => {
acc[key] = null;
return acc;
}, {});
const defaultState = {
apiKeys: initialApiKeys,
selectedModels: { llm: null, stt: null },
};
this.state = this.store.get(`users.${userId}`, defaultState);
console.log(`[ModelStateService] State loaded for user: ${userId}`);
this._autoSelectAvailableModels();
this._saveState();
this._logCurrentSelection();
}
_saveState() {
const userId = this.authService.getCurrentUserId();
this.store.set(`users.${userId}`, this.state);
console.log(`[ModelStateService] State saved for user: ${userId}`);
this._logCurrentSelection();
}
async validateApiKey(provider, key) {
if (!key || key.trim() === '') {
return { success: false, error: 'API key cannot be empty.' };
}
let validationUrl, headers;
const body = undefined;
switch (provider) {
case 'openai':
validationUrl = 'https://api.openai.com/v1/models';
headers = { 'Authorization': `Bearer ${key}` };
break;
case 'gemini':
validationUrl = `https://generativelanguage.googleapis.com/v1beta/models?key=${key}`;
headers = {};
break;
case 'anthropic': {
if (!key.startsWith('sk-ant-')) {
throw new Error('Invalid Anthropic key format.');
}
const response = await fetch("https://api.anthropic.com/v1/messages", {
method: "POST",
headers: {
"Content-Type": "application/json",
"x-api-key": key,
"anthropic-version": "2023-06-01",
},
body: JSON.stringify({
model: "claude-3-haiku-20240307",
max_tokens: 1,
messages: [{ role: "user", content: "Hi" }],
}),
});
if (!response.ok && response.status !== 400) {
const errorData = await response.json().catch(() => ({}));
return { success: false, error: errorData.error?.message || `Validation failed with status: ${response.status}` };
}
console.log(`[ModelStateService] API key for ${provider} is valid.`);
this.setApiKey(provider, key);
return { success: true };
}
default:
return { success: false, error: 'Unknown provider.' };
}
try {
const response = await fetch(validationUrl, { headers, body });
if (response.ok) {
console.log(`[ModelStateService] API key for ${provider} is valid.`);
this.setApiKey(provider, key);
return { success: true };
} else {
const errorData = await response.json().catch(() => ({}));
const message = errorData.error?.message || `Validation failed with status: ${response.status}`;
console.log(`[ModelStateService] API key for ${provider} is invalid: ${message}`);
return { success: false, error: message };
}
} catch (error) {
console.error(`[ModelStateService] Network error during ${provider} key validation:`, error);
return { success: false, error: 'A network error occurred during validation.' };
}
}
setFirebaseVirtualKey(virtualKey) {
console.log(`[ModelStateService] Setting Firebase virtual key (for openai-glass).`);
this.state.apiKeys['openai-glass'] = virtualKey;
const llmModels = PROVIDERS['openai-glass']?.llmModels;
const sttModels = PROVIDERS['openai-glass']?.sttModels;
if (!this.state.selectedModels.llm && llmModels?.length > 0) {
this.state.selectedModels.llm = llmModels[0].id;
}
if (!this.state.selectedModels.stt && sttModels?.length > 0) {
this.state.selectedModels.stt = sttModels[0].id;
}
this._autoSelectAvailableModels();
this._saveState();
this._logCurrentSelection();
}
setApiKey(provider, key) {
if (provider in this.state.apiKeys) {
this.state.apiKeys[provider] = key;
const llmModels = PROVIDERS[provider]?.llmModels;
const sttModels = PROVIDERS[provider]?.sttModels;
if (!this.state.selectedModels.llm && llmModels?.length > 0) {
this.state.selectedModels.llm = llmModels[0].id;
}
if (!this.state.selectedModels.stt && sttModels?.length > 0) {
this.state.selectedModels.stt = sttModels[0].id;
}
this._saveState();
this._logCurrentSelection();
return true;
}
return false;
}
getApiKey(provider) {
return this.state.apiKeys[provider] || null;
}
getAllApiKeys() {
const { 'openai-glass': _, ...displayKeys } = this.state.apiKeys;
return displayKeys;
}
removeApiKey(provider) {
if (provider in this.state.apiKeys) {
this.state.apiKeys[provider] = null;
const llmProvider = this.getProviderForModel('llm', this.state.selectedModels.llm);
if (llmProvider === provider) this.state.selectedModels.llm = null;
const sttProvider = this.getProviderForModel('stt', this.state.selectedModels.stt);
if (sttProvider === provider) this.state.selectedModels.stt = null;
this._autoSelectAvailableModels();
this._saveState();
this._logCurrentSelection();
return true;
}
return false;
}
getProviderForModel(type, modelId) {
if (!modelId) return null;
for (const providerId in PROVIDERS) {
const models = type === 'llm' ? PROVIDERS[providerId].llmModels : PROVIDERS[providerId].sttModels;
if (models.some(m => m.id === modelId)) {
return providerId;
}
}
return null;
}
getCurrentProvider(type) {
const selectedModel = this.state.selectedModels[type];
return this.getProviderForModel(type, selectedModel);
}
isLoggedInWithFirebase() {
return this.authService.getCurrentUser().isLoggedIn;
}
areProvidersConfigured() {
if (this.isLoggedInWithFirebase()) return true;
// LLM과 STT 모델을 제공하는 Provider 중 하나라도 API 키가 설정되었는지 확인
const hasLlmKey = Object.entries(this.state.apiKeys).some(([provider, key]) => key && PROVIDERS[provider]?.llmModels.length > 0);
const hasSttKey = Object.entries(this.state.apiKeys).some(([provider, key]) => key && PROVIDERS[provider]?.sttModels.length > 0);
return hasLlmKey && hasSttKey;
}
getAvailableModels(type) {
const available = [];
const modelList = type === 'llm' ? 'llmModels' : 'sttModels';
Object.entries(this.state.apiKeys).forEach(([providerId, key]) => {
if (key && PROVIDERS[providerId]?.[modelList]) {
available.push(...PROVIDERS[providerId][modelList]);
}
});
return [...new Map(available.map(item => [item.id, item])).values()];
}
getSelectedModels() {
return this.state.selectedModels;
}
setSelectedModel(type, modelId) {
const provider = this.getProviderForModel(type, modelId);
if (provider && this.state.apiKeys[provider]) {
this.state.selectedModels[type] = modelId;
this._saveState();
return true;
}
return false;
}
/**
*
* @param {('llm' | 'stt')} type
* @returns {{provider: string, model: string, apiKey: string} | null}
*/
getCurrentModelInfo(type) {
this._logCurrentSelection();
const model = this.state.selectedModels[type];
if (!model) {
return null;
}
const provider = this.getProviderForModel(type, model);
if (!provider) {
return null;
}
const apiKey = this.getApiKey(provider);
return { provider, model, apiKey };
}
setupIpcHandlers() {
ipcMain.handle('model:validate-key', (e, { provider, key }) => this.validateApiKey(provider, key));
ipcMain.handle('model:get-all-keys', () => this.getAllApiKeys());
ipcMain.handle('model:set-api-key', (e, { provider, key }) => this.setApiKey(provider, key));
ipcMain.handle('model:remove-api-key', (e, { provider }) => {
const success = this.removeApiKey(provider);
if (success) {
const selectedModels = this.getSelectedModels();
if (!selectedModels.llm || !selectedModels.stt) {
webContents.getAllWebContents().forEach(wc => {
wc.send('force-show-apikey-header');
});
}
}
return success;
});
ipcMain.handle('model:get-selected-models', () => this.getSelectedModels());
ipcMain.handle('model:set-selected-model', (e, { type, modelId }) => this.setSelectedModel(type, modelId));
ipcMain.handle('model:get-available-models', (e, { type }) => this.getAvailableModels(type));
ipcMain.handle('model:are-providers-configured', () => this.areProvidersConfigured());
ipcMain.handle('model:get-current-model-info', (e, { type }) => this.getCurrentModelInfo(type));
ipcMain.handle('model:get-provider-config', () => {
const serializableProviders = {};
for (const key in PROVIDERS) {
const { handler, ...rest } = PROVIDERS[key];
serializableProviders[key] = rest;
}
return serializableProviders;
});
}
}
module.exports = ModelStateService;