모델 발견 및 캐싱
ModelDiscoveryManager는 provider 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 |
| Config 병합 | packages/desktop/app/main/services/capabilities/llm/config/ (mergeModelLists) |
아키텍처 컨텍스트
graph TB
subgraph LLMConfigService
MDM[ModelDiscoveryManager]
AMM[AgentModelsManager]
end
subgraph CacheLayer ["Cache Layer"]
SQLiteCache[(SQLite<br/>llm_model_cache)]
FileCache[(File cache<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 ["Data Sources"]
ProviderConfig[Provider config<br/>modelConfigs / models]
Templates[PROVIDER_TEMPLATES<br/>built-in templates]
Presets[Provider Presets<br/>preset configs]
Mappings[PROVIDER_MODEL_MAPPINGS<br/>model mappings]
end
MDM -->|read/write| SQLiteCache
MDM -->|fallback| FileCache
MDM -->|API calls| ProviderAPI
MDM -->|fallback models| ProviderConfig
MDM -->|fallback models| Templates
AMM --> ChainCache
AMM --> Mappings
데이터 구조
모델 발견 결과
// Single model discovery entry
interface ProviderModelDiscoveryEntry {
id: string; // Model ID (e.g. 'gpt-4o')
name: string; // Display name
description?: string; // Description
contextLength?: number; // Context window length
maxTokens?: number; // Max output tokens
category?: string; // Category: 'chat' | 'reasoning' | 'image' | ...
capabilities?: string[]; // Capability tags: 'vision' | 'function_call' | 'reasoning'
}
// Discovery result
interface ProviderModelDiscoveryResult {
success: boolean;
source: string; // 'api' | 'cache' | 'fallback'
endpoint: string; // Actual API endpoint called
models: ProviderModelDiscoveryEntry[];
raw?: unknown; // Raw API response
fetchedAt?: string; // ISO timestamp
error?: string;
}
// Available model list (merged)
interface AvailableModelsResult {
models: ProviderModelDiscoveryEntry[];
source: string;
lastUpdated?: string;
}
Transformer 체인 캐시
// Chain cache entry in LLMConfigService
interface ChainCacheEntry {
chain: ResolvedTransformerChain;
cachedAt: number; // Cache timestamp
}
// Cache TTL
const CHAIN_CACHE_TTL_MS = 10 * 60 * 1000; // 10 minutes
모델 라우팅 설정
// Router configuration
interface RouterConfig {
default?: string; // Default model ("providerId,modelId")
background?: string; // Background model
think?: string; // Reasoning model
longContext?: string; // Long-context model
longContextThreshold?: number; // Long-context trigger threshold
webSearch?: string; // Search model
image?: string; // Image model
vision?: string; // Vision model
followProviderBackground?: boolean; // Background model follows primary model's provider
followProviderVision?: boolean; // Vision model follows primary model's provider
}
// Code ↔ Chat routing
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[Get provider config]
GetProvider --> LoadPreset[Load preset models<br/>getFallbackModels]
LoadPreset --> DeriveEndpoint[Derive API endpoint<br/>deriveModelsEndpoint]
DeriveEndpoint --> HasEndpoint{Valid endpoint?}
HasEndpoint -->|No| CheckCache[Check cache]
HasEndpoint -->|Yes| CheckForce{Force refresh?}
CheckForce -->|Yes| CallAPI[Call API]
CheckForce -->|No| CheckCacheAge[Check cache freshness]
CheckCacheAge --> HasFreshCache{Cache valid?}
HasFreshCache -->|Yes| ReturnCache[Return cached result]
HasFreshCache -->|No| CallAPI
CallAPI --> APISuccess{API succeeded?}
APISuccess -->|Yes| ParseModels[Parse model list]
APISuccess -->|No| FallbackCache[Use cache/preset fallback]
ParseModels --> MergeModels[Merge API results + preset models]
MergeModels --> WriteCache[Write to cache]
WriteCache --> ReturnResult[Return result]
CheckCache --> HasCacheAtAll{Cache exists?}
HasCacheAtAll -->|Yes| ReturnCache
HasCacheAtAll -->|No| ReturnFallback[Return preset models]
FallbackCache --> HasCacheAtAll
상세 단계:
discoverModels(providerId, options?):
1. provider = delegate.getProvider(providerId)
If not found → return { success: false, error: "not found" }
2. presetModels = getFallbackModels(provider)
// Look up in order: modelConfigs → models → PROVIDER_TEMPLATES
3. endpoint = deriveModelsEndpoint(provider)
// Derive from modelsEndpoint or api_base_url
4. If no valid endpoint:
Try to return from cache; otherwise return preset models (source: 'fallback')
5. If not a forced refresh:
Check cache → if valid → return cached result
6. Call provider API:
- OpenAI compatible: GET /v1/models
- Anthropic: GET /v1/models
- Gemini: GET /v1beta/models
Request is made via Electron's net module
7. Parse response, extract model list
Handle response format differences across providers
8. Merge: API-discovered models + preset models (deduplicated)
9. Write to cache (SQLite or file)
10. Return { success: true, source: 'api', models, ... }
대체 모델 해석 (getFallbackModels)
API 발견을 사용할 수 없을 때는 다음 우선순위에 따라 대체합니다.
getFallbackModels(provider):
fallbackModels = []
// Priority 1: Provider's own modelConfigs
if provider.modelConfigs?.length:
convertModelConfigs(provider.modelConfigs)
// Extract id, name, contextLength, maxTokens, capabilities
// Priority 2: Provider's own models array
else if provider.models?.length:
convertModelsArray(provider.models)
// Only id, no detailed info
// Priority 3: Matching built-in template
if fallbackModels.length === 0:
template = PROVIDER_TEMPLATES.find(matching provider.id or provider.name)
if template:
// Use template's modelConfigs or models
모델 카테고리 매핑:
| 카테고리 | 설명 |
|---|---|
chat | 일반 대화 모델(기본값) |
reasoning | 추론 모델(o1, Claude thinking) |
image | 이미지 생성 모델 |
video | 동영상 생성 모델 |
embedding | 임베딩 모델 |
code | 코드 생성 모델 |
캐싱 전략
이중 계층 캐시(SQLite + 파일)
flowchart LR
subgraph ReadPath ["Read Path"]
Read[readModelCache] --> CheckSQLite{useSQLite?}
CheckSQLite -->|Yes| SQLiteRead[db.llmModelCacheGet]
CheckSQLite -->|No| FileRead[fs.readFile]
SQLiteRead -->|failure| FileRead
end
subgraph WritePath ["Write Path"]
Write[writeModelCache] --> CheckSQLite2{useSQLite?}
CheckSQLite2 -->|Yes| SQLiteWrite[db.llmModelCacheSet]
CheckSQLite2 -->|No| FileWrite[fs.writeFile]
SQLiteWrite -->|failure| FileWrite
end
SQLite 캐시 테이블:
| 필드 | 타입 | 설명 |
|---|---|---|
| providerId | TEXT | 기본 키 |
| endpoint | TEXT | API endpoint |
| 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 minutes
getTransformerChain(key):
cached = chainCache.get(key)
if cached && (Date.now() - cached.cachedAt < TTL):
return cached.chain
// Otherwise re-parse
chain = transformerService.resolve(...)
chainCache.set(key, { chain, cachedAt: Date.now() })
return chain
모델 라우팅
Chat → Code / Code → Chat 라우팅
resolveRoutedModel(providerId, modelId):
config = loadConfig()
// Check Code → Chat routing
for route in config.routers.codeToChat:
if route.providerId === providerId && route.modelId === modelId:
return { actualProviderId: route.actualProviderId, actualModelId: route.actualModelId }
// Check Chat → Code routing
for route in config.routers.chatToCode:
if route.providerId === providerId && route.modelId === modelId:
return { actualProviderId: route.actualProviderId, actualModelId: route.actualModelId }
// No matching route
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)
// Use PROVIDER_MODEL_MAPPINGS to find the corresponding model
if followProviderBackground: background = mapping.background
if followProviderVision: vision = mapping.vision
return { background, vision }
Provider Search Config (PROVIDER_SEARCH_CONFIGS)
각 provider가 웹 검색을 구현하는 방식을 정의합니다.
| 검색 유형 | 설명 | 예시 provider |
|---|---|---|
model-param | 요청 파라미터로 검색 활성화 | DashScope, Baidu |
builtin-tool | 내장 도구 정의 주입 | Kimi, Volcengine |
mcp | MCP server를 통해 검색 도구 제공 | Custom MCP |
sdk-native | SDK 네이티브 지원(서버 측 도구) | Anthropic |
none | 검색 미지원 | Ollama |
Provider Model Mappings (PROVIDER_MODEL_MAPPINGS)
Follow-Provider 기능에서 동일 provider의 background/vision 모델을 자동으로 선택하는 데 사용됩니다.
| Provider | 기본 모델 | Background 모델 | Vision 모델 |
|---|---|---|---|
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 | 각 provider에 포함된 모델 목록 반환 |
확장 지점
새 Provider에 모델 발견 지원 추가
- provider 템플릿에
modelsEndpoint가 설정되어 있는지 확인합니다(예:/v1/models) - provider가 비표준 모델 API 형식을 사용하는 경우
ModelDiscoveryManager.discoverModels()에 파싱 로직을 추가합니다 utils.ts의deriveModelsEndpoint()에 URL 파생 규칙을 추가합니다
Follow-Provider 모델 매핑 추가
packages/desktop/app/shared/provider-presets.ts의 PROVIDER_MODEL_MAPPINGS에 추가합니다.
// Pseudocode
PROVIDER_MODEL_MAPPINGS['newProvider'] = {
primary: 'main-model-id',
background: 'lightweight-model-id',
vision: 'vision-model-id', // null if no vision model
};
캐시 TTL 사용자 지정
- 모델 발견 캐시: SQLite의
expiresAt필드로 제어됩니다(현재 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 사용 |