Внутреннее устройство ProviderManager
ProviderManager управляет жизненным циклом LLM-провайдеров, включая операции создания, чтения, обновления и удаления, а также систему шаблонов-пресетов и постоянное хранилище данных.
Расположение файлов
| Файл | Путь |
|---|---|
| ProviderManager | packages/desktop/app/main/services/capabilities/llm/config-service/ProviderManager.ts |
| LLMConfigService | packages/desktop/app/main/services/capabilities/llm/config-service/LLMConfigService.ts |
| Шаблоны провайдеров | packages/desktop/app/shared/llm-config.ts (PROVIDER_TEMPLATES) |
| Пресеты провайдеров | packages/desktop/app/shared/provider-presets.ts |
| Схемы | packages/desktop/app/main/services/capabilities/llm/config-service/schemas.ts |
| IPC-роутер | packages/desktop/app/main/services/routers/llm/ProviderRouter.ts |
Архитектурный контекст
Паттерн делегирования
LLMConfigService использует паттерн делегирования для распределения ответственности между четырьмя специализированными менеджерами. ProviderManager взаимодействует с хост-сервисом через интерфейс ProviderManagerDelegate, чтобы избежать циклических зависимостей.
graph TB
subgraph LLMConfigService
direction TB
PM[ProviderManager]
MDM[ModelDiscoveryManager]
CIOM[ConfigIOManager]
AMM[AgentModelsManager]
end
PM -->|Delegate| LLMConfigService
MDM -->|Delegate| LLMConfigService
CIOM -->|Delegate| LLMConfigService
AMM -->|Delegate| LLMConfigService
subgraph Storage ["Хранилище"]
SQLite[(SQLite — основное хранилище)]
JSON[(JSON — резервное хранилище)]
end
PM --> SQLite
PM --> JSON
subgraph Cache ["Кэш"]
PI[Индекс провайдеров<br/>Map-индекс]
end
PM --> PI
Интерфейс делегирования
interface ProviderManagerDelegate {
loadConfig(): Promise<LLMsConfig>; // Загрузить конфигурацию из JSON
saveConfig(): Promise<void>; // Сохранить конфигурацию в JSON
getDb(): DbClient | null; // Получить экземпляр базы данных
waitForMigration(): Promise<void>; // Ждать завершения миграции данных
useSQLite(): boolean; // Использовать ли SQLite
isMigrated(): Promise<boolean>; // Завершена ли миграция
invalidateProviderIndex(): void; // Инвалидировать индекс провайдеров
invalidateConfig(): void; // Инвалидировать кэш конфигурации
buildProviderIndex(providers: LLMProvider[]): void; // Построить индекс
getProviderFromIndex(id: string): LLMProvider | undefined; // Запросить индекс
hasProviderIndex(): boolean; // Существует ли индекс
}
Структуры данных
Полное определение LLMProvider
interface LLMProvider {
id: string; // UUID или ID пресета (например, 'openai', 'anthropic')
name: string; // Отображаемое имя
apiFormat?: ApiFormat; // Предпочтительный формат API
chatApiFormat?: ApiFormat; // Устаревшее поле формата (для обратной совместимости)
apiType?: string; // Устаревшее поле типа
api_base_url?: string; // Базовый URL API
api_key: string; // Значение ключа (устаревшее; новая система использует ApiKeyPool)
models: string[]; // Список поддерживаемых ID моделей
modelConfigs?: ModelConfig[]; // Детальная конфигурация для каждой модели
modelGroups?: ModelGroup[]; // Группы моделей (с информацией о категории)
modelsEndpoint?: string; // Эндпоинт API для обнаружения моделей
enabled: boolean; // Включён ли провайдер
transformer?: TransformerConfig; // Цепочка трансформаторов запроса/ответа
icon?: string; // Путь к иконке или эмодзи
website?: string; // Ссылка на официальный сайт
docsUrl?: string; // Ссылка на документацию API
defaultSettings?: CompletionSettings; // Настройки генерации по умолчанию
codingPlan?: CodingPlanConfig; // Специальная конфигурация Coding Plan
isSystem?: boolean; // Флаг встроенного системного провайдера (нельзя удалить)
presetId?: string; // ID исходного шаблона-пресета
headers?: Record<string, string>; // Пользовательские HTTP-заголовки
createdAt: string; // Временная метка ISO
updatedAt: string; // Временная метка ISO
}
interface ModelConfig {
id: string;
name: string;
enabled: boolean;
category?: 'chat' | 'reasoning' | 'image' | 'video' | 'embedding' | 'code';
group?: string;
contextLength?: number;
maxTokens?: number;
completionSettings?: CompletionSettings;
vision?: boolean; // Поддержка зрительного восприятия (vision)
functionCall?: boolean; // Поддержка вызова функций
reasoning?: boolean; // Поддержка рассуждений
webSearch?: boolean; // Поддержка веб-поиска
}
Алгоритмы и логика
Поток операций CRUD для провайдеров
Добавление провайдера (путь SQLite)
1. Получить LLMProviderInput
2. Сгенерировать UUID в качестве provider.id
3. Заполнить значения по умолчанию (enabled=false, defaultSettings=DEFAULT_COMPLETION_SETTINGS)
4. Валидировать с помощью Zod Schema (LLMProviderSchema.parse)
5. Проверка уникальности: запросить SQLite, чтобы убедиться, что ID не существует
6. Записать в SQLite: db.llmProvidersInsert(provider)
7. Синхронизировать список моделей в таблицу llm_models
8. Синхронизировать группы моделей в таблицу llm_model_groups
9. Инвалидировать индекс провайдеров: invalidateProviderIndex()
10. Вернуть { success: true, provider }
Обновление провайдера
1. Получить { id, ...updates }
2. Прочитать существующего провайдера из SQLite
3. Смёржить обновлённые поля (поверхностное слияние)
4. Обновить временную метку updatedAt
5. Записать в SQLite: db.llmProvidersUpdate(id, updates)
6. Если изменились models или modelConfigs → синхронизировать таблицы моделей
7. Инвалидировать индекс провайдеров
8. Вернуть { success: true, provider }
Удаление провайдера
1. Проверить флаг isSystem → встроенные системные провайдеры нельзя удалить
2. Удалить из SQLite: db.llmProvidersDelete(id)
3. Каскадно удалить связанные модели и группы
4. Инвалидировать индекс провайдеров
5. Вернуть { success: true }
Индекс провайдеров (поиск за O(1))
graph LR
subgraph FirstQuery ["Первый запрос"]
Load[loadConfig или запрос SQLite] --> Build[Построить Map]
Build --> Cache[providerIndex: Map<id, Provider>]
end
subgraph SubsequentQuery ["Последующие запросы"]
Cache --> Lookup[O(1) Map.get]
end
subgraph Mutations ["Мутации"]
CRUD[add/update/delete] --> Invalidate[providerIndex = null]
Invalidate --> Load
end
Правила поведения:
| Операция | Воздействие на индекс |
|---|---|
getProvider(id) | Попадание в индекс → вернуть; промах → перестроить индекс, затем найти |
getProviders() | Вернуть полный список и перестроить индекс |
addProvider | Инвалидировать индекс |
updateProvider | Инвалидировать индекс |
deleteProvider | Инвалидировать индекс |
toggleProvider | Инвалидировать индекс |
Механизм двойного хранилища (SQLite + резервный JSON)
flowchart TD
Start[Запуск] --> CheckDB{DbClient доступен?}
CheckDB -->|Да| Migrate[ensureMigrated]
CheckDB -->|Нет| JSONMode[Режим JSON]
Migrate --> CheckMigrated{Уже мигрировано?}
CheckMigrated -->|Да| SQLiteMode[Режим SQLite]
CheckMigrated -->|Нет| DoMigrate[Миграция JSON → SQLite]
DoMigrate --> SQLiteMode
subgraph SQLiteMode ["Режим SQLite"]
SQLiteMode --> SQLiteRead[db.llmProvidersGetAll]
SQLiteMode --> SQLiteWrite[db.llmProvidersInsert/Update/Delete]
end
subgraph JSONMode ["Режим JSON"]
JSONMode --> JSONRead[fs.readFile + JSON.parse]
JSONMode --> JSONWrite[JSON.stringify + fs.writeFile]
end
Процесс миграции:
- При запуске проверить флаг
llm_config_migratedв SQLite - Если миграция ещё не выполнена — прочитать
llms-config.json - Записать каждую запись провайдера в SQLite по одной
- Синхронизировать модели и modelGroups в соответствующие таблицы
- Установить флаг завершения миграции
- JSON-файл сохраняется как резервная копия (больше не является основным хранилищем)
Система пресетов / шаблонов
Список провайдеров по умолчанию
Система включает следующие встроенные шаблоны провайдеров (в порядке, определённом в getDefaultProviders()):
| # | ID | Имя | Формат API |
|---|---|---|---|
| 1 | deepseek | DeepSeek | openai |
| 2 | openrouter | OpenRouter | openai |
| 3 | silicon | SiliconFlow | openai |
| 4 | openai | OpenAI | openai |
| 5 | anthropic | Anthropic | anthropic |
| 6 | gemini | Google Gemini | |
| 7 | zhipu | Zhipu AI | openai |
| 8 | moonshot | Moonshot | openai |
| 9 | dashscope | Tongyi Qianwen | openai |
| 10 | ollama | Ollama | openai |
| 11 | groq | Groq | openai |
| 12 | claude-code | Claude Code | anthropic |
Создание провайдера из шаблона
createProviderFromTemplate(template):
1. Скопировать все поля конфигурации из шаблона
2. Установить api_key = '' (пользователь должен настроить его сам)
3. Установить enabled = false
4. Заполнить defaultSettings (использовать template.defaultSettings или DEFAULT_COMPLETION_SETTINGS)
5. Сгенерировать временные метки createdAt / updatedAt
6. Вернуть экземпляр LLMProvider
Пресеты провайдеров
Шаблоны-пресеты берутся из provider-presets.ts и предлагают более богатые конфигурации по сравнению со встроенными шаблонами, как правило, нацеленные на китайских облачных вендоров. Шаблон-пресет включает:
interface PresetProviderTemplate {
id: string; // ID пресета
name: string; // Отображаемое имя
apiFormat: ApiFormat; // Формат API
baseUrl: string; // Базовый URL
models: string[]; // Список моделей
modelConfigs: ModelConfig[]; // Детальная конфигурация для каждой модели
features: string[]; // Теги возможностей
// ... другие поля
}
Добавление провайдера из пресета:
addFromPreset(presetId, apiKey?):
1. Найти пресет через getPresetById(presetId)
2. Преобразовать пресет в ProviderTemplate
3. Вызвать createProviderFromTemplate(template)
4. Если передан apiKey → установить ключ
5. Вызвать addProvider() для сохранения
6. Вернуть нового провайдера
Таблица интеграции IPC
| IPC-канал | Направление | Параметры | Возвращаемое значение | Валидация Zod |
|---|---|---|---|---|
llmConfig:getProviders | R → M | Нет | LLMProvider[] | Нет |
llmConfig:getProvider | R → M | { id: string } | LLMProvider | null | llmProviderIdSchema |
llmConfig:addProvider | R → M | LLMProviderInput | LLMProviderResult | llmProviderCreateSchema |
llmConfig:updateProvider | R → M | { id, ...fields } | LLMProviderResult | llmProviderUpdateSchema |
llmConfig:deleteProvider | R → M | { id: string } | { success, message? } | llmProviderIdSchema |
llmConfig:toggleProvider | R → M | { id, enabled } | LLMProviderResult | id + boolean |
llmConfig:discoverModels | R → M | { id, options? } | ProviderModelDiscoveryResult | modelDiscoverOptionsSchema |
llmConfig:getProviderPresets | R → M | Нет | PresetProviderTemplate[] | Нет |
llmConfig:addFromPreset | R → M | { presetId, apiKey? } | LLMProviderResult | string + string? |
llmConfig:exportConfig | R → M | Нет | LLMsConfig | Нет |
llmConfig:importConfig | R → M | Объект конфигурации | { success, message? } | Zod object |
Точки расширения
Добавление нового встроенного провайдера
- Добавить
ProviderTemplateв массивPROVIDER_TEMPLATESвpackages/desktop/app/shared/llm-config.ts - Добавить новый ID в массив
defaultTemplateIdsвProviderManager.getDefaultProviders() - Если требуется особая логика построения URL — добавить обработку в
url-builder.ts - Если нужна поддержка поиска — добавить запись в
PROVIDER_SEARCH_CONFIGSвprovider-presets.ts - Если нужна поддержка Coding Plan — добавить запись в
CODING_PLAN_URL_PRESETS - Если нужна поддержка Follow-Provider — добавить запись в
PROVIDER_MODEL_MAPPINGS
Добавление нового шаблона-пресета
- Добавить
PresetProviderTemplateв массивPROVIDER_PRESETSвprovider-presets.ts - Заполнить полные конфигурации моделей (modelConfigs, contextLength, maxTokens, возможности)
- Фронтенд автоматически отобразит новый шаблон в списке пресетов
Связанные файлы
| Файл | Связь |
|---|---|
capabilities/llm/config-service/LLMConfigService.ts | Хост-сервис, который инициализирует ProviderManager и предоставляет Delegate |
capabilities/llm/config-service/schemas.ts | Схемы валидации Zod (LLMProviderSchema и др.) |
routers/llm/ProviderRouter.ts | IPC-слой: получает запросы от фронтенда и вызывает LLMConfigService |
routers/llm/schemas.ts | Схемы валидации параметров IPC |
shared/llm-config.ts | Общие определения типов и встроенные шаблоны |
shared/provider-presets.ts | Шаблоны-пресеты, конфиги поиска, URL для Coding Plan |
workers/DbClient.ts | Операции с базой данных SQLite |
workers/db/apiKeys.ts | CRUD для таблицы API-ключей |
capabilities/llm/completion/CompletionService.ts | Потребляет конфигурацию провайдера для вызовов API |