メインコンテンツまでスキップ

CompletionService コールパイプライン

CompletionService は LLM 呼び出しの中核となるファサードです。フロントエンドの Completion リクエストを、完全な API 呼び出しパイプラインとしてオーケストレーションします。このドキュメントでは、6 ステップのパイプライン、ストリーミング処理、思考バジェット計算、リトライ機構、ツールコールループについて詳しく説明します。


ファイルの場所

ファイルパス
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

データ構造

リクエスト型とレスポンス型

// 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_VARprocess.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非ストリーミングハンドラーストリーミングハンドラー
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) // 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 に思考トークンが含まれるため、次の処理が必要です。

  1. 思考バジェット thinkingBudget を計算する
  2. max_tokens から思考バジェットを差し引き、adjustedMaxTokens を取得する
  3. リクエストボディに含める 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_ITERATIONSglobalParams.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:completeR → MCompletionRouter非ストリーミング Completion
completion:getModelsR → MCompletionRouter利用可能なモデルを取得
completion:testModelR → MCompletionRouterモデル接続をテスト
ストリーミング CompletionR → MChatStreamHandlerIPC メッセージ経由で配信される SSE ストリーム
再生成R → MRegenerateHandler返信を再生成

拡張ポイント

新しい API フォーマットの追加

  1. types.tsApiFormat 型に新しい値を追加する
  2. url-builder.ts に URL ビルダー関数を追加する
  3. header-builder.ts にヘッダー構築ロジックを追加する
  4. message-converter.ts にメッセージフォーマット変換を追加する
  5. DirectApiHandler.tscallXxxCompletion 関数を追加する
  6. StreamHandler.tsstreamXxxCompletion 関数を追加する
  7. 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.tsSSE ストリーム解析ユーティリティ
shared/completion-types.tsSimpleChatMessage などの共有型
shared/thinking-config.ts思考バジェット計算と推論モデル検出
shared/llm-config.tsLLMProvider 型定義
capabilities/tools/mcp-users/McpService.tsツール実行(ToolHandler から呼び出し)
capabilities/llm/api-converter/openai-to-anthropic.tsOpenAI → Anthropic フォーマット変換
routers/CompletionRouter.tsIPC エントリーポイント