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

Конвейер вызовов CompletionService

CompletionService — это основной фасад для вызовов LLM. Он преобразует запросы на завершение от фронтенда в полный конвейер вызовов API. В данном документе подробно описан шестишаговый конвейер, обработка потоковой передачи, вычисление бюджета мышления, механизм повторных попыток и цикл вызовов инструментов.


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

ФайлПуть
CompletionServicepackages/desktop/app/main/services/capabilities/llm/completion/CompletionService.ts
DirectApiHandlerpackages/desktop/app/main/services/capabilities/llm/completion/DirectApiHandler.ts
StreamHandlerpackages/desktop/app/main/services/capabilities/llm/completion/StreamHandler.ts
ToolHandlerpackages/desktop/app/main/services/capabilities/llm/completion/ToolHandler.ts
TransformerHandlerpackages/desktop/app/main/services/capabilities/llm/completion/TransformerHandler.ts
ThinkingResolverpackages/desktop/app/main/services/capabilities/llm/completion/ThinkingResolver.ts
URL Builderpackages/desktop/app/main/services/capabilities/llm/completion/url-builder.ts
Header Builderpackages/desktop/app/main/services/capabilities/llm/completion/header-builder.ts
Message Converterpackages/desktop/app/main/services/capabilities/llm/completion/message-converter.ts
Typespackages/desktop/app/main/services/capabilities/llm/completion/types.ts
NativeSearchInjectorpackages/desktop/app/main/services/capabilities/llm/completion/NativeSearchInjector.ts
ProviderSearchInjectorpackages/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_VARprocess.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Обработчик без потокаПотоковый обработчик
openaicallOpenAICompletionstreamOpenAICompletion
openai-responsecallOpenAIResponseCompletionstreamOpenAIResponseCompletion
anthropiccallAnthropicCompletionstreamAnthropicCompletion
googlecallGeminiCompletionstreamGeminiCompletion
azure-openaicallOpenAICompletionstreamOpenAICompletion

Шаг 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 включает токены мышления, поэтому требуется:

  1. Вычислить бюджет мышления thinkingBudget
  2. Вычесть бюджет мышления из max_tokens, получив adjustedMaxTokens
  3. Создать объект конфигурации 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_ITERATIONSglobalParams.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:completeR → MCompletionRouterЗавершение без потока
completion:getModelsR → MCompletionRouterПолучение доступных моделей
completion:testModelR → MCompletionRouterТестирование подключения к модели
Потоковое завершениеR → MChatStreamHandlerSSE-поток, доставляемый через IPC-сообщения
РегенерацияR → MRegenerateHandlerПерегенерация ответа

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

Добавление нового формата API

  1. Добавить новое значение в тип ApiFormat в types.ts
  2. Добавить функцию построения URL в url-builder.ts
  3. Добавить логику построения заголовков в header-builder.ts
  4. Добавить преобразование формата сообщений в message-converter.ts
  5. Добавить функцию callXxxCompletion в DirectApiHandler.ts
  6. Добавить функцию streamXxxCompletion в StreamHandler.ts
  7. Добавить ветку в 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