feat(llm): add LLM call support to worker IPC chain

- ProviderManager: align API with contracts ProviderAdapter
- WorkerProtocol: add llm.request/llm.response message types
- WorkerRuntime: add call_llm() for worker→parent→LLM flow
- WorkerManager: support tool_registry and provider_manager injection

P1-1 complete, P1-2 protocol layer complete.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
AirCoding
2026-06-04 18:42:06 +08:00
parent df36c43829
commit 8fd680cf84
4 changed files with 198 additions and 23 deletions

View File

@@ -7,13 +7,11 @@
* @module packages/llm/src/ProviderManager
*/
import type { ProviderAdapter } from '@aircoding/contracts'
import type { ProviderAdapter, ProviderCompletionInput, ProviderStreamEvent, ProviderCapabilityMatrix, ModelID, ProviderID } from '@aircoding/contracts'
// Local type definitions (contract types not yet finalized)
type CompleteOptions = { max_tokens?: number; temperature?: number; top_p?: number; system?: string }
type StreamEvent = { type: 'text' | 'thinking' | 'done'; content?: string; reason?: string }
type ModelRequirement = { model: string; provider?: string; min_output_tokens?: number; prefers_thinking?: boolean; requires_tools?: boolean }
type ModelAssignment = { provider: string; model: string; adapter: any; capabilities?: any }
type ModelAssignment = { provider: string; model: string; adapter: ProviderAdapter; capabilities?: ReturnType<CapabilityMatrixRegistry['lookup']> }
import { ModelConfigLoader, createModelConfigLoader } from './ModelConfigLoader.js'
import { CapabilityMatrixRegistry, createCapabilityMatrixRegistry } from './CapabilityMatrix.js'
@@ -80,34 +78,118 @@ export class ProviderManager {
/**
* Complete a request with the current model.
* Returns AsyncIterable of ProviderStreamEvent per contracts §15.
*/
async complete(
async *complete(
input: ProviderCompletionInput
): AsyncGenerator<ProviderStreamEvent> {
const adapter = this.current_adapter || this.adapters.get(input.provider_id || 'anthropic')
if (!adapter) {
throw new Error('No adapter selected. Call select_model first.')
}
yield* adapter.complete(input)
}
/**
* Complete and collect all events into a single response.
*/
async complete_text(
messages: unknown[],
options: { model?: string; max_tokens?: number; temperature?: number; system?: string } = {}
): Promise<{ content: string; usage?: { input_tokens: number; output_tokens: number } }> {
const adapter = this.current_adapter
if (!adapter) {
throw new Error('No adapter selected. Call select_model first.')
}
const model_id = options.model || 'claude-haiku-4-5-20251001'
const input: ProviderCompletionInput = {
provider_id: 'anthropic',
model_id: model_id as ModelID,
canonical_format: 'anthropic',
messages,
max_output_tokens: options.max_tokens || 4096,
temperature: options.temperature,
system: options.system
}
let content = ''
let usage: { input_tokens: number; output_tokens: number } | undefined
for await (const event of adapter.complete(input)) {
if (event.type === 'content_delta') {
const payload = event.payload as { type: string; text?: string; thinking?: string }
if (payload.type === 'text_delta') {
content += payload.text || ''
} else if (payload.type === 'thinking_delta') {
// Accumulate thinking for reference but don't include in content
}
} else if (event.type === 'message_stop') {
const payload = event.payload as { usage?: { output_tokens: number } }
if (payload.usage) {
usage = { input_tokens: 0, output_tokens: payload.usage.output_tokens }
}
}
}
return { content, usage }
}
/**
* Stream a completion request (passthrough to adapter).
*/
async *stream_complete(
input: ProviderCompletionInput
): AsyncGenerator<ProviderStreamEvent> {
const adapter = this.current_adapter
if (!adapter) {
throw new Error('No adapter selected. Call select_model first.')
}
yield* adapter.complete(input)
}
/**
* Legacy overload: select model then complete.
*/
async complete_after_select(
messages: unknown[],
assignment: ModelAssignment,
options: CompleteOptions = {}
options: { max_tokens?: number; temperature?: number } = {}
): Promise<{ content: string; usage?: { input_tokens: number; output_tokens: number } }> {
const adapter = assignment.adapter || this.current_adapter
if (!adapter) {
throw new Error('No adapter selected. Call select_model first.')
}
return adapter.complete(messages as any, { model: assignment.model }, options)
}
/**
* Stream a completion request.
*/
async *stream_complete(
messages: unknown[],
assignment: ModelAssignment,
options: CompleteOptions = {}
): AsyncGenerator<StreamEvent> {
const adapter = assignment.adapter || this.current_adapter
if (!adapter) {
throw new Error('No adapter selected. Call select_model first.')
const input: ProviderCompletionInput = {
provider_id: assignment.provider as ProviderID || 'anthropic',
model_id: assignment.model as ModelID,
canonical_format: 'anthropic',
messages,
max_output_tokens: options.max_tokens || 4096,
temperature: options.temperature
}
yield* adapter.stream_complete(messages as any, { model: assignment.model }, options)
let content = ''
let usage: { input_tokens: number; output_tokens: number } | undefined
for await (const event of adapter.complete(input)) {
if (event.type === 'content_delta') {
const payload = event.payload as { type: string; text?: string }
if (payload.type === 'text_delta') {
content += payload.text || ''
}
} else if (event.type === 'message_stop') {
const payload = event.payload as { usage?: { output_tokens: number } }
if (payload.usage) {
usage = { input_tokens: 0, output_tokens: payload.usage.output_tokens }
}
}
}
return { content, usage }
}
/**