CompletionService コールパイプライン
CompletionService は LLM 呼び出しの中核となるファサードです。フロントエンドの Completion リクエストを、完全な API 呼び出しパイプラインとしてオーケストレーションします。このドキュメントでは、6 ステップのパイプライン、ストリーミング処理、思考バジェット計算、リトライ機構、ツールコールループについて詳しく説明します。
ファイルの場所
| ファイル | パス |
|---|---|
| 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
データ構造
リクエスト型とレスポンス型
// Completion request options
interface CompletionOptions {
providerId: string; // Provider ID
model: string; // Model ID (v89+: bare SDK id, no `<backend>:` prefix or `[1m]` suffix)
messages: SimpleChatMessage[]; // Conversation messages
maxTokens?: number; // Max tokens to generate
temperature?: number; // Temperature
stream?: boolean; // Whether streaming
thinkLevel?: ThinkLevel; // Thinking level: 'none' | 'low' | 'medium' | 'high'
nativeSearchAugmentation?: NativeSearchAugmentation; // SDK native search augmentation
sessionId?: string; // Session ID (API Key Pool affinity)
/**
* 1M-context flag (v89+). When true and the model is on the 1M-capable whitelist
* (`claude-opus-4-7` / `claude-opus-4-6` / `claude-sonnet-4-6`),
* `TransformerHandler` calls `injectExtendedContextBeta()` at the transformer chain
* exit, merging `'context-1m-2025-08-07'` into the outbound request's
* `anthropic-beta` HTTP header
* (NOT a body field; `/v1/messages` rejects unknown body fields).
*/
useExtendedContext?: boolean;
}
// Completion result
interface CompletionResult {
success: boolean;
message?: SimpleChatMessage; // Generated message
error?: string; // Error message
usage?: {
promptTokens: number;
completionTokens: number;
totalTokens: number;
};
finishReason?: string; // 'stop' | 'tool_use' | 'max_tokens' etc.
}
// Streaming callbacks
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; // Content blocks: thinking/text/tool_use/tool_result
onDone?: (message, usage?, metrics?) => void;
onError?: (error: string) => void;
}
// API format
type ApiFormat = 'openai' | 'anthropic' | 'google' | 'azure-openai' | 'openai-response';
アルゴリズムとロジック
6 ステップのリクエストパイプライン
ステップ 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 の Provider Index を通じて行われ、O(1) の検索として実行されます。
ステップ 3: API Key 解決
resolveApiKeyForRequest(provider, providerId, sessionId):
// Priority 1: Coding Plan override
if provider.codingPlan?.enabled && provider.codingPlan.apiKey:
return resolveApiKey(codingPlan.apiKey)
// Priority 2: API Key Pool (session-affinity weighted round-robin)
if apiKeyPool available:
poolKey = sessionId
? apiKeyPool.getKeyForSession(providerId, sessionId)
: apiKeyPool.getKey(providerId)
if poolKey: return poolKey
// Priority 3: Legacy single key
return resolveApiKey(provider.api_key)
resolveApiKey() は環境変数の展開($ENV_VAR → process.env.ENV_VAR)を処理します。
ステップ 4: API フォーマット解決
resolveApiFormat(provider):
// Priority 1: apiFormat field (v3 preferred)
if provider.apiFormat: return provider.apiFormat
// Priority 2: chatApiFormat field (legacy v3)
if provider.chatApiFormat: return provider.chatApiFormat
// Priority 3: implicit apiType conversion
if provider.apiType === 'claudecode' || 'anthropic': return 'anthropic'
if provider.apiType === 'google': return 'google'
// Default: OpenAI format
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) // retry once with new key
if result succeeded && apiKeyPool available:
apiKeyPool.reportSuccess(sessionId) // reset cooldown counter
ストリーミング
SSE ストリームの解析
すべてのストリーミングハンドラーは、標準の SSE(Server-Sent Events)プロトコルに基づく streamSSEResponse() ユーティリティを使用します。
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 推論モデル
OpenAI o-series モデル(o1、o3 など)では、トークンバジェットを調整する代わりに reasoning_effort パラメーターを使用します。
if thinkLevel !== 'none':
effort = getOpenAIReasoningEffort(thinkLevel)
// 'low' | 'medium' | 'high'
request.reasoning_effort = effort
max_tokens 解決の優先順位
resolveEffectiveMaxTokens(providerId, modelId, sessionMaxTokens):
// 1. Session-level setting (highest priority, user-set manually, no cap applied)
if sessionMaxTokens > 0: return sessionMaxTokens
// 2. Global model parameters (admin-set, no cap applied)
globalParams = llmConfig.getGlobalModelParameters()
if globalParams.maxTokens.enabled && value > 0: return value
// -------- Values below are auto-resolved and capped at MAX_TOKENS_CAP=65536 --------
// 3. maxTokens from model config
modelConfig = provider.modelConfigs.find(id === modelId)
if modelConfig.maxTokens > 0: return min(value, 65536)
// 4. maxTokens from model group
modelGroup = provider.modelGroups.find(models.id === modelId)
if model.maxTokens > 0: return min(value, 65536)
// 5. Model discovery cache
discovered = llmConfig.getDiscoveredModelMaxTokens(providerId, modelId)
if discovered > 0: return min(value, 65536)
// 6. undefined (let the API use its default)
return undefined
// For providers requiring max_tokens (e.g. Anthropic):
getRequiredMaxTokens():
resolved = resolveEffectiveMaxTokens(...)
return resolved ?? DEFAULT_MAX_TOKENS // typically 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 がツール要求を停止すると自然に終了 |
ツールコールは 3 つのフォーマットをサポートし、logToolFormat() によって自動検出されます。
- OpenAI フォーマット:
{ type: 'function', function: { name, parameters } } - Anthropic フォーマット:
{ name, input_schema } - Gemini フォーマット:
{ functionDeclarations: [...] }
Vision フォールバック
メッセージに画像が含まれている一方で、モデルが vision をサポートしていない場合、補助 vision モデルが自動的に使用されます。
applyVisionFallback(options):
if no images in messages: return
if model supports vision: return
visionModel = llmConfig.resolveEffectiveModels().vision
if no vision model:
// Strip images
for msg in messages:
msg.images = undefined
return
// Use VisionDescriptionService to describe images
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
// Example: "API error (429): Rate limit exceeded" → 429
IPC 連携表
| IPC チャンネル | 方向 | ルーター | 説明 |
|---|---|---|---|
completion:complete | R → M | CompletionRouter | 非ストリーミング Completion |
completion:getModels | R → M | CompletionRouter | 利用可能なモデルを取得 |
completion:testModel | R → M | CompletionRouter | モデル接続をテスト |
| ストリーミング Completion | R → M | ChatStreamHandler | IPC メッセージ経由で配信される SSE ストリーム |
| 再生成 | R → M | RegenerateHandler | 返信を再生成 |
拡張ポイント
新しい API フォーマットの追加
types.tsのApiFormat型に新しい値を追加するurl-builder.tsに URL ビルダー関数を追加するheader-builder.tsにヘッダー構築ロジックを追加するmessage-converter.tsにメッセージフォーマット変換を追加するDirectApiHandler.tsにcallXxxCompletion関数を追加するStreamHandler.tsにstreamXxxCompletion関数を追加するCompletionService.callDirectHandler()とcallStreamHandler()の switch に case を追加する
カスタムリトライ戦略
現在は 1 回だけリトライを試行します。より複雑なリトライ動作(複数回のリトライ、待機時間の変化など)が必要な場合は、complete() と completeStream() のリトライロジックを変更してください。
新しい検索注入の追加
- SDK ネイティブ検索(例: 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 エントリーポイント |