Перейти к основному содержимому

Внутреннее устройство ProviderManager

ProviderManager управляет жизненным циклом LLM-провайдеров, включая операции создания, чтения, обновления и удаления, а также систему шаблонов-пресетов и постоянное хранилище данных.


Расположение файлов

ФайлПуть
ProviderManagerpackages/desktop/app/main/services/capabilities/llm/config-service/ProviderManager.ts
LLMConfigServicepackages/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&lt;id, Provider&gt;]
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

Процесс миграции:

  1. При запуске проверить флаг llm_config_migrated в SQLite
  2. Если миграция ещё не выполнена — прочитать llms-config.json
  3. Записать каждую запись провайдера в SQLite по одной
  4. Синхронизировать модели и modelGroups в соответствующие таблицы
  5. Установить флаг завершения миграции
  6. JSON-файл сохраняется как резервная копия (больше не является основным хранилищем)

Система пресетов / шаблонов

Список провайдеров по умолчанию

Система включает следующие встроенные шаблоны провайдеров (в порядке, определённом в getDefaultProviders()):

#IDИмяФормат API
1deepseekDeepSeekopenai
2openrouterOpenRouteropenai
3siliconSiliconFlowopenai
4openaiOpenAIopenai
5anthropicAnthropicanthropic
6geminiGoogle Geminigoogle
7zhipuZhipu AIopenai
8moonshotMoonshotopenai
9dashscopeTongyi Qianwenopenai
10ollamaOllamaopenai
11groqGroqopenai
12claude-codeClaude Codeanthropic

Создание провайдера из шаблона

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:getProvidersR → MНетLLMProvider[]Нет
llmConfig:getProviderR → M{ id: string }LLMProvider | nullllmProviderIdSchema
llmConfig:addProviderR → MLLMProviderInputLLMProviderResultllmProviderCreateSchema
llmConfig:updateProviderR → M{ id, ...fields }LLMProviderResultllmProviderUpdateSchema
llmConfig:deleteProviderR → M{ id: string }{ success, message? }llmProviderIdSchema
llmConfig:toggleProviderR → M{ id, enabled }LLMProviderResultid + boolean
llmConfig:discoverModelsR → M{ id, options? }ProviderModelDiscoveryResultmodelDiscoverOptionsSchema
llmConfig:getProviderPresetsR → MНетPresetProviderTemplate[]Нет
llmConfig:addFromPresetR → M{ presetId, apiKey? }LLMProviderResultstring + string?
llmConfig:exportConfigR → MНетLLMsConfigНет
llmConfig:importConfigR → MОбъект конфигурации{ success, message? }Zod object

Точки расширения

Добавление нового встроенного провайдера

  1. Добавить ProviderTemplate в массив PROVIDER_TEMPLATES в packages/desktop/app/shared/llm-config.ts
  2. Добавить новый ID в массив defaultTemplateIds в ProviderManager.getDefaultProviders()
  3. Если требуется особая логика построения URL — добавить обработку в url-builder.ts
  4. Если нужна поддержка поиска — добавить запись в PROVIDER_SEARCH_CONFIGS в provider-presets.ts
  5. Если нужна поддержка Coding Plan — добавить запись в CODING_PLAN_URL_PRESETS
  6. Если нужна поддержка Follow-Provider — добавить запись в PROVIDER_MODEL_MAPPINGS

Добавление нового шаблона-пресета

  1. Добавить PresetProviderTemplate в массив PROVIDER_PRESETS в provider-presets.ts
  2. Заполнить полные конфигурации моделей (modelConfigs, contextLength, maxTokens, возможности)
  3. Фронтенд автоматически отобразит новый шаблон в списке пресетов

Связанные файлы

ФайлСвязь
capabilities/llm/config-service/LLMConfigService.tsХост-сервис, который инициализирует ProviderManager и предоставляет Delegate
capabilities/llm/config-service/schemas.tsСхемы валидации Zod (LLMProviderSchema и др.)
routers/llm/ProviderRouter.tsIPC-слой: получает запросы от фронтенда и вызывает LLMConfigService
routers/llm/schemas.tsСхемы валидации параметров IPC
shared/llm-config.tsОбщие определения типов и встроенные шаблоны
shared/provider-presets.tsШаблоны-пресеты, конфиги поиска, URL для Coding Plan
workers/DbClient.tsОперации с базой данных SQLite
workers/db/apiKeys.tsCRUD для таблицы API-ключей
capabilities/llm/completion/CompletionService.tsПотребляет конфигурацию провайдера для вызовов API