Конвейер вызовов CompletionService
CompletionService — это основной фасад для вызовов LLM. Он преобразует запросы на завершение от фронтенда в полный конвейер вызовов API. В данном документе подробно описан шестишаговый конвейер, обработка потоковой передачи, вычисление бюджета мышления, механизм повторных попыток и цикл вызовов инструментов.
Расположение файлов
| Файл | Путь |
|---|---|
| CompletionService | packages/desktop/app/main/services/capabilities/llm/completion/CompletionService.ts |
| DirectApiHandler | packages/desktop/app/main/services/capabilities/llm/completion/DirectApiHandler.ts |
| StreamHandler | packages/desktop/app/main/services/capabilities/llm/completion/StreamHandler.ts |
| ToolHandler | packages/desktop/app/main/services/capabilities/llm/completion/ToolHandler.ts |
| TransformerHandler | packages/desktop/app/main/services/capabilities/llm/completion/TransformerHandler.ts |
| ThinkingResolver | packages/desktop/app/main/services/capabilities/llm/completion/ThinkingResolver.ts |
| URL Builder | packages/desktop/app/main/services/capabilities/llm/completion/url-builder.ts |
| Header Builder | packages/desktop/app/main/services/capabilities/llm/completion/header-builder.ts |
| Message Converter | packages/desktop/app/main/services/capabilities/llm/completion/message-converter.ts |
| Types | packages/desktop/app/main/services/capabilities/llm/completion/types.ts |
| NativeSearchInjector | packages/desktop/app/main/services/capabilities/llm/completion/NativeSearchInjector.ts |
| ProviderSearchInjector | packages/desktop/app/main/services/capabilities/llm/completion/ProviderSearchInjector.ts |
Архитектурный контекст
graph TB
subgraph CompletionService ["CompletionService (Facade)"]
direction TB
Complete[complete]
Stream[completeStream]
WithTransformers[completeWithTransformers]
StreamTransformers[completeStreamWithTransformers]
WithTools[streamWithTools]
TestModel[testModel]
end
subgraph PipelineSteps ["Pipeline Steps"]
direction TB
S1["(1) Route resolution<br/>resolveRoutedModel"]
S2["(2) Provider lookup<br/>getProvider + enabled check"]
S3["(3) API Key resolution<br/>codingPlan → pool → legacy"]
S4["(4) API format resolution<br/>resolveApiFormat"]
S5["(5) Handler dispatch<br/>callDirectHandler / callStreamHandler"]
S6["(6) Retry + success report"]
end
subgraph Handlers
DAH[DirectApiHandler<br/>non-streaming]
SH[StreamHandler<br/>SSE streaming]
TH[ToolHandler<br/>tool loop]
THR[TransformerHandler<br/>transformer chain]
end
subgraph AuxiliaryServices ["Auxiliary Services"]
TR2[ThinkingResolver]
NSI[NativeSearchInjector]
PSI[ProviderSearchInjector]
MC[Message Converter]
UB[URL Builder]
HB[Header Builder]
end
Complete --> S1 --> S2 --> S3 --> S4 --> S5 --> S6
S5 --> DAH
S5 --> SH
WithTools --> TH
WithTransformers --> THR
StreamTransformers --> THR
DAH --> UB
DAH --> HB
DAH --> MC
SH --> UB
SH --> HB
SH --> MC
SH --> TR2
TH --> UB
TH --> HB
Структуры данных
Типы запросов и ответов
// Параметры запроса на завершение
interface CompletionOptions {
providerId: string; // ID провайдера
model: string; // ID модели (v89+: чистый SDK id, без префикса `<backend>:` и суффикса `[1m]`)
messages: SimpleChatMessage[]; // Сообщения диалога
maxTokens?: number; // Максимальное количество генерируемых токенов
temperature?: number; // Температура
stream?: boolean; // Использовать ли потоковую передачу
thinkLevel?: ThinkLevel; // Уровень мышления: 'none' | 'low' | 'medium' | 'high'
nativeSearchAugmentation?: NativeSearchAugmentation; // SDK native search augmentation
sessionId?: string; // ID сессии (привязка к пулу API-ключей)
/**
* Флаг контекста 1M (v89+). При значении true и наличии модели в белом списке
* (`claude-opus-4-7` / `claude-opus-4-6` / `claude-sonnet-4-6`),
* `TransformerHandler` вызывает `injectExtendedContextBeta()` на выходе
* цепочки трансформеров, добавляя `'context-1m-2025-08-07'` в HTTP-заголовок
* `anthropic-beta` исходящего запроса
* (НЕ поле тела; `/v1/messages` отклоняет неизвестные поля тела).
*/
useExtendedContext?: boolean;
}
// Результат завершения
interface CompletionResult {
success: boolean;
message?: SimpleChatMessage; // Сгенерированное сообщение
error?: string; // Сообщение об ошибке
usage?: {
promptTokens: number;
completionTokens: number;
totalTokens: number;
};
finishReason?: string; // 'stop' | 'tool_use' | 'max_tokens' и др.
}
// Коллбэки потоковой передачи
interface StreamCallbacks {
onStart?: (messageId: string) => void;
onDelta?: (content: string) => void;
onReasoning?: (reasoning: string) => void;
onAudio?: (audio: SimpleChatAudio) => void;
onVideo?: (video: SimpleChatVideo) => void;
onBlock?: (block: MessageBlock) => void; // Блоки содержимого: thinking/text/tool_use/tool_result
onDone?: (message, usage?, metrics?) => void;
onError?: (error: string) => void;
}
// Формат API
type ApiFormat = 'openai' | 'anthropic' | 'google' | 'azure-openai' | 'openai-response';
Алгоритмы и логика
Шестишаговый конвейер запросов
Шаг 1: Разрешение маршрута
routedInfo = llmConfig.resolveRoutedModel(providerId, model)
actualProviderId = routedInfo?.actualProviderId || providerId
actualModel = routedInfo?.actualModelId || model
Разрешение маршрута обрабатывает маршрутизацию Chat → Code и Code → Chat. Если model соответствует правилу маршрутизации, она заменяется фактическим провайдером и моделью.
Шаг 2: Поиск провайдера
provider = getProvider(actualProviderId)
if (!provider) → return error "Provider not found"
if (!provider.enabled) → return error "Provider is disabled"
Поиск выполняется через индекс провайдеров LLMConfigService за время O(1).
Шаг 3: Разрешение API-ключа
resolveApiKeyForRequest(provider, providerId, sessionId):
// Приоритет 1: переопределение через Coding Plan
if provider.codingPlan?.enabled && provider.codingPlan.apiKey:
return resolveApiKey(codingPlan.apiKey)
// Приоритет 2: пул API-ключей (взвешенный round-robin с привязкой к сессии)
if apiKeyPool available:
poolKey = sessionId
? apiKeyPool.getKeyForSession(providerId, sessionId)
: apiKeyPool.getKey(providerId)
if poolKey: return poolKey
// Приоритет 3: устаревший единственный ключ
return resolveApiKey(provider.api_key)
resolveApiKey() обрабатывает раскрытие переменных среды ($ENV_VAR → process.env.ENV_VAR).
Шаг 4: Разрешение формата API
resolveApiFormat(provider):
// Приоритет 1: поле apiFormat (предпочтительно v3)
if provider.apiFormat: return provider.apiFormat
// Приоритет 2: поле chatApiFormat (устаревшее v3)
if provider.chatApiFormat: return provider.chatApiFormat
// Приоритет 3: неявное преобразование apiType
if provider.apiType === 'claudecode' || 'anthropic': return 'anthropic'
if provider.apiType === 'google': return 'google'
// По умолчанию: формат OpenAI
return 'openai'
Шаг 5: Диспетчеризация обработчика
Выбор подходящего обработчика на основе apiFormat:
| apiFormat | Обработчик без потока | Потоковый обработчик |
|---|---|---|
openai | callOpenAICompletion | streamOpenAICompletion |
openai-response | callOpenAIResponseCompletion | streamOpenAIResponseCompletion |
anthropic | callAnthropicCompletion | streamAnthropicCompletion |
google | callGeminiCompletion | streamGeminiCompletion |
azure-openai | callOpenAICompletion | streamOpenAICompletion |
Шаг 6: Повторная попытка и отчёт об успехе
result = callHandler(...)
if result failed && apiKeyPool available && sessionId present:
status = extractHttpStatus(result.error)
if status in [429, 529, 401, 403]:
newKey = apiKeyPool.reportError(providerId, sessionId, status)
if newKey:
result = callHandler(..., newKey) // однократная повторная попытка с новым ключом
if result succeeded && apiKeyPool available:
apiKeyPool.reportSuccess(sessionId) // сброс счётчика ожидания
Потоковая передача
Разбор потока SSE
Все потоковые обработчики используют утилиту streamSSEResponse(), основанную на стандартном протоколе SSE (Server-Sent Events):
sequenceDiagram
participant CS as CompletionService
participant SH as StreamHandler
participant API as Provider API
CS->>SH: callStreamHandler(format, provider, key, options)
SH->>API: POST request (stream: true)
API-->>SH: SSE stream
loop Each SSE event
SH->>SH: Parse event data
alt content delta
SH->>CS: callbacks.onDelta(content)
else reasoning delta
SH->>CS: callbacks.onReasoning(reasoning)
else block event
SH->>CS: callbacks.onBlock(block)
else [DONE]
SH->>CS: callbacks.onDone(message, usage, metrics)
else error
SH->>CS: callbacks.onError(error)
end
end
Повторная попытка при потоке (режим пула)
flowchart TD
Start[completeStream] --> HasPool{apiKeyPool available?}
HasPool -->|No| DirectCall[Call callStreamHandler directly]
HasPool -->|Yes| InterceptCall[Call with intercepting callbacks]
InterceptCall --> StreamDone{Stream completed successfully?}
StreamDone -->|Yes| ReportSuccess[reportSuccess]
StreamDone -->|No| CheckStatus{Is 429/529/401/403?}
CheckStatus -->|Yes| GetNewKey[reportError → get new key]
CheckStatus -->|No| PropagatError[Propagate error to frontend]
GetNewKey --> HasNewKey{New key available?}
HasNewKey -->|Yes| RetryStream[Retry stream with new key]
HasNewKey -->|No| PropagatError
Ключевые особенности повторных попыток при потоке:
- Ошибки 429/529 перехватываются путём обёртывания
callbacks.onError - Для отслеживания состояния ошибки через замыкания используется объект
retryState - При повторной попытке
onStartне вызывается снова (был вызван уже однажды)
Вычисление бюджета мышления Anthropic
flowchart TD
Start[resolveThinkingBudget] --> CheckLevel{thinkLevel === 'none'?}
CheckLevel -->|Yes| NoThinking[Return raw maxTokens<br/>no thinking config]
CheckLevel -->|No| CheckModel{isReasoningModel?}
CheckModel -->|No| NoThinking
CheckModel -->|Yes| CalcBudget[calculateThinkingBudget<br/>model, thinkLevel, maxTokens]
CalcBudget --> CheckFormat{API format is Anthropic?}
CheckFormat -->|Yes| AdjustTokens[adjustedMaxTokens = getClaudeMaxTokens<br/>maxTokens - thinkingBudget]
CheckFormat -->|No| KeepTokens[adjustedMaxTokens = maxTokens]
AdjustTokens --> BuildConfig[buildAnthropicThinking<br/>generate thinking config object]
BuildConfig --> Return[Return adjustedMaxTokens + thinkingConfig]
KeepTokens --> Return
Особая обработка Anthropic: для моделей Claude параметр max_tokens включает токены мышления, поэтому требуется:
- Вычислить бюджет мышления
thinkingBudget - Вычесть бюджет мышления из
max_tokens, получивadjustedMaxTokens - Создать объект конфигурации
thinkingдля включения в тело запроса
Рассуждающие модели OpenAI
Для моделей серии o (o1, o3 и др.) вместо корректировки бюджета токенов используется параметр reasoning_effort:
if thinkLevel !== 'none':
effort = getOpenAIReasoningEffort(thinkLevel)
// 'low' | 'medium' | 'high'
request.reasoning_effort = effort
Приоритет разрешения max_tokens
resolveEffectiveMaxTokens(providerId, modelId, sessionMaxTokens):
// 1. Настройка уровня сессии (наивысший приоритет, задаётся пользователем вручную, ограничение не применяется)
if sessionMaxTokens > 0: return sessionMaxTokens
// 2. Глобальные параметры модели (задаётся администратором, ограничение не применяется)
globalParams = llmConfig.getGlobalModelParameters()
if globalParams.maxTokens.enabled && value > 0: return value
// -------- Значения ниже разрешаются автоматически и ограничены MAX_TOKENS_CAP=65536 --------
// 3. maxTokens из конфигурации модели
modelConfig = provider.modelConfigs.find(id === modelId)
if modelConfig.maxTokens > 0: return min(value, 65536)
// 4. maxTokens из группы моделей
modelGroup = provider.modelGroups.find(models.id === modelId)
if model.maxTokens > 0: return min(value, 65536)
// 5. Кэш обнаружения моделей
discovered = llmConfig.getDiscoveredModelMaxTokens(providerId, modelId)
if discovered > 0: return min(value, 65536)
// 6. undefined (API использует значение по умолчанию)
return undefined
// Для провайдеров, требующих max_tokens (например, Anthropic):
getRequiredMaxTokens():
resolved = resolveEffectiveMaxTokens(...)
return resolved ?? DEFAULT_MAX_TOKENS // обычно 4096
Цикл вызовов инструментов (ToolHandler)
sequenceDiagram
participant TH as ToolHandler
participant LLM as LLM API
participant MCP as MCP Service
TH->>TH: MAX_ITERATIONS = globalParams.toolMaxTurns ?? 5
TH->>TH: iteration = 0
loop iteration < MAX_ITERATIONS
TH->>TH: iteration++
TH->>TH: buildToolRequest(format, messages, model, options)
TH->>LLM: POST request with tools
LLM-->>TH: SSE stream response
TH->>TH: extractToolCalls(response)
alt No tool calls
TH->>TH: break (LLM finished answering)
else Has tool calls
loop Each tool call
TH->>TH: callbacks.onToolCall(toolCall)
TH->>MCP: executeToolCalls(toolCalls, mcpService)
MCP-->>TH: tool results
TH->>TH: callbacks.onToolResult(id, result)
end
TH->>TH: Append tool calls and results to messages
TH->>TH: buildIterationBlocks(tool call blocks)
end
end
TH->>TH: callbacks.onDone(finalContent, usage)
Ключевые параметры:
| Параметр | По умолчанию | Описание |
|---|---|---|
MAX_ITERATIONS | globalParams.toolMaxTurns ?? 5 | Максимальное количество итераций |
| Формат инструмента | Автоопределение по apiFormat | Определения инструментов в формате OpenAI/Anthropic/Gemini |
| Условие завершения | Нет вызовов инструментов или достигнут лимит | Завершается естественным образом, когда LLM перестаёт запрашивать инструменты |
Вызовы инструментов поддерживают три формата, автоопределяемых через logToolFormat():
- Формат OpenAI:
{ type: 'function', function: { name, parameters } } - Формат Anthropic:
{ name, input_schema } - Формат Gemini:
{ functionDeclarations: [...] }
Резервный вариант для зрения (Vision Fallback)
Если сообщения содержат изображения, но модель не поддерживает зрение, автоматически используется вспомогательная модель зрения:
applyVisionFallback(options):
if no images in messages: return
if model supports vision: return
visionModel = llmConfig.resolveEffectiveModels().vision
if no vision model:
// Убрать изображения
for msg in messages:
msg.images = undefined
return
// Используем VisionDescriptionService для описания изображений
for msg in messages with images:
description = visionService.describeImages(images, msg.content, visionModel)
msg.content += "\n\n[Image Description]\n" + description
msg.images = undefined
Шаблон перехвата ошибок
extractHttpStatus() извлекает HTTP-код статуса из строки сообщения об ошибке:
extractHttpStatus(error: string):
match = error.match(/\((\d{3})\):/)
return match ? parseInt(match[1]) : null
// Пример: "API error (429): Rate limit exceeded" → 429
Таблица интеграции IPC
| Канал IPC | Направление | Маршрутизатор | Описание |
|---|---|---|---|
completion:complete | R → M | CompletionRouter | Завершение без потока |
completion:getModels | R → M | CompletionRouter | Получение доступных моделей |
completion:testModel | R → M | CompletionRouter | Тестирование подключения к модели |
| Потоковое завершение | R → M | ChatStreamHandler | SSE-поток, доставляемый через IPC-сообщения |
| Регенерация | R → M | RegenerateHandler | Перегенерация ответа |
Точки расширения
Добавление нового формата API
- Добавить новое значение в тип
ApiFormatвtypes.ts - Добавить функцию построения URL в
url-builder.ts - Добавить логику построения заголовков в
header-builder.ts - Добавить преобразование формата сообщений в
message-converter.ts - Добавить функцию
callXxxCompletionвDirectApiHandler.ts - Добавить функцию
streamXxxCompletionвStreamHandler.ts - Добавить ветку в switch в
CompletionService.callDirectHandler()иcallStreamHandler()
Пользовательская стратегия повторных попыток
В настоящее время выполняется только одна повторная попытка. Для более сложного поведения (например, несколько попыток, переменное время ожидания) измените логику повторных попыток в complete() и completeStream().
Добавление нового поискового инжектора
- SDK native search (например, Anthropic): инжектировать через
NativeSearchInjector.applyAugmentation() - Поиск, специфичный для провайдера (например, model-param / builtin-tool): инжектировать через
ProviderSearchInjector
Связанные файлы
| Файл | Связь |
|---|---|
capabilities/llm/config-service/LLMConfigService.ts | Предоставляет поиск провайдеров и разрешение маршрутов |
capabilities/llm/completion/ApiKeyPoolService.ts | Выбор ключей и балансировка нагрузки |
infra/utils/sse-parser.ts | Утилиты разбора SSE-потока |
shared/completion-types.ts | Общие типы, такие как SimpleChatMessage |
shared/thinking-config.ts | Вычисление бюджета мышления и определение рассуждающей модели |
shared/llm-config.ts | Определение типа LLMProvider |
capabilities/tools/mcp-users/McpService.ts | Выполнение инструментов (вызывается из ToolHandler) |
capabilities/llm/api-converter/openai-to-anthropic.ts | Преобразование формата OpenAI → Anthropic |
routers/CompletionRouter.ts | Точка входа IPC |