Обнаружение и кэширование моделей
ModelDiscoveryManager динамически обнаруживает доступные списки моделей из API провайдеров, управляет кэшем результатов обнаружения и работает совместно с AgentModelsManager для реализации маршрутизации моделей и функции Follow-Provider.
Расположение файлов
| Файл | Путь |
|---|---|
| ModelDiscoveryManager | packages/desktop/app/main/services/capabilities/llm/config-service/ModelDiscoveryManager.ts |
| AgentModelsManager | packages/desktop/app/main/services/capabilities/llm/config-service/AgentModelsManager.ts |
| LLMConfigService | packages/desktop/app/main/services/capabilities/llm/config-service/LLMConfigService.ts |
| Provider Presets | packages/desktop/app/shared/provider-presets.ts |
| Вспомогательные функции | packages/desktop/app/main/services/capabilities/llm/config-service/utils.ts |
| Слияние конфигурации | packages/desktop/app/main/services/capabilities/llm/config/ (mergeModelLists) |
Архитектурный контекст
graph TB
subgraph LLMConfigService
MDM[ModelDiscoveryManager]
AMM[AgentModelsManager]
end
subgraph CacheLayer ["Слой кэша"]
SQLiteCache[(SQLite<br/>llm_model_cache)]
FileCache[(Файловый кэш<br/>model-cache/*.json)]
ChainCache["Transformer Chain Cache<br/>Map (10min TTL)"]
end
subgraph ProviderAPI ["Provider API"]
OpenAIModels["/v1/models"]
AnthropicModels["/v1/models"]
GeminiModels["/v1beta/models"]
end
subgraph DataSources ["Источники данных"]
ProviderConfig[Конфигурация провайдера<br/>modelConfigs / models]
Templates[PROVIDER_TEMPLATES<br/>встроенные шаблоны]
Presets[Provider Presets<br/>предустановленные конфиги]
Mappings[PROVIDER_MODEL_MAPPINGS<br/>маппинг моделей]
end
MDM -->|read/write| SQLiteCache
MDM -->|fallback| FileCache
MDM -->|API calls| ProviderAPI
MDM -->|fallback models| ProviderConfig
MDM -->|fallback models| Templates
AMM --> ChainCache
AMM --> Mappings
Структуры данных
Результаты обнаружения моделей
// Одна запись обнаружения модели
interface ProviderModelDiscoveryEntry {
id: string; // ID модели (например, 'gpt-4o')
name: string; // Отображаемое название
description?: string; // Описание
contextLength?: number; // Длина контекстного окна
maxTokens?: number; // Максимум выходных токенов
category?: string; // Категория: 'chat' | 'reasoning' | 'image' | ...
capabilities?: string[]; // Теги возможностей: 'vision' | 'function_call' | 'reasoning'
}
// Результат обнаружения
interface ProviderModelDiscoveryResult {
success: boolean;
source: string; // 'api' | 'cache' | 'fallback'
endpoint: string; // Фактически вызванный API-эндпоинт
models: ProviderModelDiscoveryEntry[];
raw?: unknown; // Необработанный ответ API
fetchedAt?: string; // Метка времени в формате ISO
error?: string;
}
// Список доступных моделей (объединённый)
interface AvailableModelsResult {
models: ProviderModelDiscoveryEntry[];
source: string;
lastUpdated?: string;
}
Кэш цепочки Transformer
// Запись кэша цепочки в LLMConfigService
interface ChainCacheEntry {
chain: ResolvedTransformerChain;
cachedAt: number; // Метка времени кэширования
}
// TTL кэша
const CHAIN_CACHE_TTL_MS = 10 * 60 * 1000; // 10 минут
Конфигурация маршрутизации моделей
// Конфигурация маршрутизатора
interface RouterConfig {
default?: string; // Модель по умолчанию ("providerId,modelId")
background?: string; // Фоновая модель
think?: string; // Модель рассуждений
longContext?: string; // Модель для длинного контекста
longContextThreshold?: number; // Порог активации длинного контекста
webSearch?: string; // Модель для поиска
image?: string; // Модель для изображений
vision?: string; // Модель компьютерного зрения
followProviderBackground?: boolean; // Фоновая модель следует провайдеру основной модели
followProviderVision?: boolean; // Модель зрения следует провайдеру основной модели
}
// Маршрутизация Code ↔ Chat
interface CodeToChatRouterConfig {
providerId: string;
modelId: string;
actualProviderId: string;
actualModelId: string;
}
interface ChatToCodeRouterConfig {
providerId: string;
modelId: string;
actualProviderId: string;
actualModelId: string;
}
Алгоритмы и логика
Процесс обнаружения моделей
flowchart TD
Start[discoverModels] --> GetProvider[Получить конфигурацию провайдера]
GetProvider --> LoadPreset[Загрузить предустановленные модели<br/>getFallbackModels]
LoadPreset --> DeriveEndpoint[Определить API-эндпоинт<br/>deriveModelsEndpoint]
DeriveEndpoint --> HasEndpoint{Эндпоинт действителен?}
HasEndpoint -->|Нет| CheckCache[Проверить кэш]
HasEndpoint -->|Да| CheckForce{Принудительное обновление?}
CheckForce -->|Да| CallAPI[Вызвать API]
CheckForce -->|Нет| CheckCacheAge[Проверить актуальность кэша]
CheckCacheAge --> HasFreshCache{Кэш действителен?}
HasFreshCache -->|Да| ReturnCache[Вернуть кэшированный результат]
HasFreshCache -->|Нет| CallAPI
CallAPI --> APISuccess{API успешен?}
APISuccess -->|Да| ParseModels[Разобрать список моделей]
APISuccess -->|Нет| FallbackCache[Использовать кэш/предустановленный fallback]
ParseModels --> MergeModels[Объединить результаты API + предустановленные модели]
MergeModels --> WriteCache[Записать в кэш]
WriteCache --> ReturnResult[Вернуть результат]
CheckCache --> HasCacheAtAll{Кэш существует?}
HasCacheAtAll -->|Да| ReturnCache
HasCacheAtAll -->|Нет| ReturnFallback[Вернуть предустановленные модели]
FallbackCache --> HasCacheAtAll
Подробные шаги:
discoverModels(providerId, options?):
1. provider = delegate.getProvider(providerId)
Если не найден → вернуть { success: false, error: "not found" }
2. presetModels = getFallbackModels(provider)
// Поиск по порядку: modelConfigs → models → PROVIDER_TEMPLATES
3. endpoint = deriveModelsEndpoint(provider)
// Определить из modelsEndpoint или api_base_url
4. Если нет действительного эндпоинта:
Попытаться вернуть из кэша; иначе вернуть предустановленные модели (source: 'fallback')
5. Если не принудительное обновление:
Проверить кэш → если действителен → вернуть кэшированный результат
6. Вызвать API провайдера:
- Совместимый с OpenAI: GET /v1/models
- Anthropic: GET /v1/models
- Gemini: GET /v1beta/models
Запрос выполняется через модуль net в Electron
7. Разобрать ответ, извлечь список моделей
Обработать различия в формате ответа разных провайдеров
8. Объединить: модели, обнаруженные через API + предустановленные модели (с дедупликацией)
9. Записать в кэш (SQLite или файл)
10. Вернуть { success: true, source: 'api', models, ... }
Разрешение резервных моделей (getFallbackModels)
Когда обнаружение через API недоступно, используется следующий порядок приоритетов:
getFallbackModels(provider):
fallbackModels = []
// Приоритет 1: Собственный modelConfigs провайдера
if provider.modelConfigs?.length:
convertModelConfigs(provider.modelConfigs)
// Извлечь id, name, contextLength, maxTokens, capabilities
// Приоритет 2: Собственный массив models провайдера
else if provider.models?.length:
convertModelsArray(provider.models)
// Только id, без подробной информации
// Приоритет 3: Совпадающий встроенный шаблон
if fallbackModels.length === 0:
template = PROVIDER_TEMPLATES.find(совпадение с provider.id или provider.name)
if template:
// Использовать modelConfigs или models из шаблона
Маппинг категорий моделей:
| Категория | Описание |
|---|---|
chat | Модель для общего диалога (по умолчанию) |
reasoning | Модель рассуждений (o1, Claude thinking) |
image | Модель генерации изображений |
video | Модель генерации видео |
embedding | Модель эмбеддингов |
code | Модель генерации кода |
Стратегия кэширования
Двухуровневый кэш (SQLite + файл)
flowchart LR
subgraph ReadPath ["Путь чтения"]
Read[readModelCache] --> CheckSQLite{useSQLite?}
CheckSQLite -->|Да| SQLiteRead[db.llmModelCacheGet]
CheckSQLite -->|Нет| FileRead[fs.readFile]
SQLiteRead -->|сбой| FileRead
end
subgraph WritePath ["Путь записи"]
Write[writeModelCache] --> CheckSQLite2{useSQLite?}
CheckSQLite2 -->|Да| SQLiteWrite[db.llmModelCacheSet]
CheckSQLite2 -->|Нет| FileWrite[fs.writeFile]
SQLiteWrite -->|сбой| FileWrite
end
Таблица кэша SQLite:
| Поле | Тип | Описание |
|---|---|---|
| providerId | TEXT | Первичный ключ |
| endpoint | TEXT | API-эндпоинт |
| source | TEXT | Источник ('api' / 'cache') |
| models | JSON | Сериализованный список моделей |
| raw | JSON | Необработанный ответ API |
| fetchedAt | INTEGER | Метка времени получения |
| expiresAt | INTEGER | Метка времени истечения (24 часа) |
Файловый кэш:
- Каталог:
userData/model-cache/ - Имя файла:
{providerId}.json(специальные символы заменяются подчёркиванием) - Нет механизма истечения (зависит от принудительного обновления)
Кэш цепочки Transformer
LLMConfigService поддерживает кэш в памяти для цепочек Transformer, чтобы избежать повторного разбора:
chainCache: Map<string, { chain: ResolvedTransformerChain, cachedAt: number }>
CHAIN_CACHE_TTL_MS = 10 * 60 * 1000 // 10 минут
getTransformerChain(key):
cached = chainCache.get(key)
if cached && (Date.now() - cached.cachedAt < TTL):
return cached.chain
// Иначе выполнить повторный разбор
chain = transformerService.resolve(...)
chainCache.set(key, { chain, cachedAt: Date.now() })
return chain
Маршрутизация моделей
Маршрутизация Chat → Code / Code → Chat
resolveRoutedModel(providerId, modelId):
config = loadConfig()
// Проверить маршрутизацию Code → Chat
for route in config.routers.codeToChat:
if route.providerId === providerId && route.modelId === modelId:
return { actualProviderId: route.actualProviderId, actualModelId: route.actualModelId }
// Проверить маршрутизацию Chat → Code
for route in config.routers.chatToCode:
if route.providerId === providerId && route.modelId === modelId:
return { actualProviderId: route.actualProviderId, actualModelId: route.actualModelId }
// Нет совпадающего маршрута
return null
Маппинг моделей Follow-Provider
resolveEffectiveModels():
router = config.router
agentDefaults = config.agentDefaultModels
background = agentDefaults.background
vision = router.vision
if (followProviderBackground || followProviderVision) && router.default:
defaultProviderId = router.default.split(',')[0]
provider = getProvider(defaultProviderId)
if provider:
mapping = resolveFollowProviderModel(provider)
// Использовать PROVIDER_MODEL_MAPPINGS для поиска соответствующей модели
if followProviderBackground: background = mapping.background
if followProviderVision: vision = mapping.vision
return { background, vision }
Конфигурация поиска провайдера (PROVIDER_SEARCH_CONFIGS)
Определяет, как каждый провайдер реализует веб-поиск:
| Тип поиска | Описание | Примеры провайдеров |
|---|---|---|
model-param | Включить поиск через параметр запроса | DashScope, Baidu |
builtin-tool | Внедрить определение встроенного инструмента | Kimi, Volcengine |
mcp | Предоставить инструменты поиска через MCP-сервер | Кастомный MCP |
sdk-native | Нативная поддержка SDK (серверные инструменты) | Anthropic |
none | Поиск не поддерживается | Ollama |
Маппинг моделей провайдера (PROVIDER_MODEL_MAPPINGS)
Используется функцией Follow-Provider для автоматического выбора фоновой/зрительной модели того же провайдера:
| Провайдер | Основная модель | Фоновая модель | Зрительная модель |
|---|---|---|---|
zhipu | glm-5 | glm-4.5-air | glm-4.6v |
volcengine | ark-code-latest | doubao-seed-2.0-lite | doubao-seed-2.0-code |
kimi | kimi-k2.5 | kimi-k2-0905-preview | kimi-k2.5 |
| ... | ... | ... | ... |
Таблица интеграции IPC
| IPC-канал | Направление | Описание |
|---|---|---|
llmConfig:discoverModels | R → M | Запустить обнаружение моделей (поддерживает параметр forceRefresh) |
llmConfig:getProviders | R → M | Вернуть список моделей, включённых в каждый провайдер |
Точки расширения
Добавление поддержки обнаружения моделей для нового провайдера
- Убедиться, что в шаблоне провайдера задан
modelsEndpoint(например,/v1/models) - Если провайдер использует нестандартный формат API моделей, добавить логику разбора в
ModelDiscoveryManager.discoverModels() - Добавить правила определения URL в
deriveModelsEndpoint()файлаutils.ts
Добавление маппинга моделей Follow-Provider
Добавить в PROVIDER_MODEL_MAPPINGS в файле packages/desktop/app/shared/provider-presets.ts:
// Псевдокод
PROVIDER_MODEL_MAPPINGS['newProvider'] = {
primary: 'main-model-id',
background: 'lightweight-model-id',
vision: 'vision-model-id', // null если модель зрения отсутствует
};
Настройка TTL кэша
- Кэш обнаружения моделей: управляется полем
expiresAtв SQLite (в настоящее время 24 часа) - Кэш цепочки Transformer: изменить
LLMConfigService.CHAIN_CACHE_TTL_MS(в настоящее время 10 минут)
Связанные файлы
| Файл | Связь |
|---|---|
capabilities/llm/config-service/LLMConfigService.ts | Хост-сервис, инициализирующий ModelDiscoveryManager |
capabilities/llm/config-service/utils.ts | Утилиты: deriveModelsEndpoint, resolveApiKey и др. |
capabilities/llm/config-service/AgentModelsManager.ts | Маршрутизация моделей, разрешение Follow-Provider |
shared/llm-config.ts | PROVIDER_TEMPLATES, определения типов |
shared/provider-presets.ts | PROVIDER_MODEL_MAPPINGS, PROVIDER_SEARCH_CONFIGS |
capabilities/llm/config/model-discovery.ts | Утилита слияния/дедупликации списков моделей (mergeModelLists) |
workers/DbClient.ts | Чтение/запись кэша SQLite |
routers/llm/ProviderRouter.ts | IPC-слой discoverModels |
capabilities/llm/completion/ThinkingResolver.ts | Использует discoveredModelMaxTokens |