はじめに
実用的な AI プラットフォームは、多くの LLM プロバイダーをサポートする必要があります。 OpenAI をハードコードすることはできません。顧客はローカルで Claude、Gemini、または Ollama を使用したい場合があります。 LLM Router は、アダプター パターンを使用してこの問題を解決します。
1.LLMAdapter インターフェイス
// packages/core/src/llm/types.ts
export interface LLMAdapter {
readonly provider: string;
chat(
messages: LLMMessage[],
tools?: ToolDefinition[],
options?: LLMOptions,
): Promise<LLMResponse>;
chatStream(
messages: LLMMessage[],
tools?: ToolDefinition[],
options?: LLMOptions,
): AsyncGenerator<StreamEvent>;
listModels(): Promise<ModelInfo[]>;
}
export interface LLMOptions {
temperature?: number;
maxTokens?: number;
topP?: number;
stop?: string[];
jsonMode?: boolean;
}
export interface ModelInfo {
id: string;
name: string;
provider: string;
contextWindow: number;
pricing?: { inputPer1k: number; outputPer1k: number };
}
すべての LLM プロバイダーはこのインターフェイスを実装する必要があります。これは アダプター パターン です。
2. OpenAI アダプター
// packages/core/src/llm/adapters/openai-adapter.ts
import OpenAI from 'openai';
export class OpenAIAdapter implements LLMAdapter {
readonly provider = 'openai';
private client: OpenAI;
constructor(config: { apiKey: string; baseUrl?: string }) {
this.client = new OpenAI({
apiKey: config.apiKey,
baseURL: config.baseUrl,
});
}
async chat(messages: LLMMessage[], tools?: ToolDefinition[]): Promise<LLMResponse> {
const response = await this.client.chat.completions.create({
model: 'gpt-4o',
messages: this.convertMessages(messages),
tools: tools ? this.convertTools(tools) : undefined,
});
const choice = response.choices[0];
return {
content: choice.message.content ?? '',
toolCalls: choice.message.tool_calls?.map(tc => ({
id: tc.id,
name: tc.function.name,
arguments: JSON.parse(tc.function.arguments),
})),
usage: response.usage ? {
promptTokens: response.usage.prompt_tokens,
completionTokens: response.usage.completion_tokens,
totalTokens: response.usage.total_tokens,
} : undefined,
};
}
async *chatStream(messages: LLMMessage[], tools?: ToolDefinition[]): AsyncGenerator<StreamEvent> {
const stream = await this.client.chat.completions.create({
model: 'gpt-4o',
messages: this.convertMessages(messages),
tools: tools ? this.convertTools(tools) : undefined,
stream: true,
});
for await (const chunk of stream) {
const delta = chunk.choices[0]?.delta;
if (delta?.content) {
yield { type: 'text-delta', delta: delta.content };
}
if (delta?.tool_calls) {
for (const tc of delta.tool_calls) {
if (tc.function?.name) {
yield { type: 'tool-call-start', toolCallId: tc.id!, toolName: tc.function.name };
}
if (tc.function?.arguments) {
yield { type: 'tool-call-args', toolCallId: tc.id!, args: tc.function.arguments };
}
}
}
}
yield { type: 'finish' };
}
private convertMessages(messages: LLMMessage[]) {
// Convert platform-agnostic format → OpenAI format
return messages.map(m => ({
role: m.role as 'system' | 'user' | 'assistant' | 'tool',
content: m.content,
tool_calls: m.toolCalls?.map(tc => ({
id: tc.id,
type: 'function' as const,
function: { name: tc.name, arguments: JSON.stringify(tc.arguments) },
})),
tool_call_id: m.toolCallId,
}));
}
private convertTools(tools: ToolDefinition[]) {
return tools.map(t => ({
type: 'function' as const,
function: {
name: t.name,
description: t.description,
parameters: t.parameters,
},
}));
}
}
3. LLM ルーター — ルーティング ロジック
// packages/core/src/llm/llm-router.ts
export type TaskComplexity = 'fast' | 'smart' | 'cheap';
export class LLMRouter {
private adapters = new Map<string, LLMAdapter>();
private fallbackChains: Record<TaskComplexity, string[]> = {
fast: ['groq', 'openai', 'anthropic'],
smart: ['anthropic', 'openai', 'google'],
cheap: ['ollama', 'groq', 'openai'],
};
register(adapter: LLMAdapter) {
this.adapters.set(adapter.provider, adapter);
}
// Resolve chain — tìm provider available theo priority
resolveChain(complexity: TaskComplexity): LLMAdapter {
const chain = this.fallbackChains[complexity];
for (const provider of chain) {
const adapter = this.adapters.get(provider);
if (adapter) return adapter;
}
// Fallback: lấy bất kỳ adapter nào available
const first = this.adapters.values().next().value;
if (!first) throw new Error('No LLM providers configured');
return first;
}
async chat(
messages: LLMMessage[],
tools?: ToolDefinition[],
complexity: TaskComplexity = 'smart',
): Promise<LLMResponse> {
const adapter = this.resolveChain(complexity);
try {
return await adapter.chat(messages, tools);
} catch (error) {
// Auto-fallback to next provider
const chain = this.fallbackChains[complexity];
const currentIndex = chain.indexOf(adapter.provider);
for (let i = currentIndex + 1; i < chain.length; i++) {
const fallback = this.adapters.get(chain[i]);
if (fallback) {
console.warn(`Falling back from ${adapter.provider} to ${chain[i]}`);
return await fallback.chat(messages, tools);
}
}
throw error;
}
}
}
4. 人間と Google アダプター
// packages/core/src/llm/adapters/anthropic-adapter.ts
export class AnthropicAdapter implements LLMAdapter {
readonly provider = 'anthropic';
async chat(messages: LLMMessage[], tools?: ToolDefinition[]): Promise<LLMResponse> {
// Anthropic API has a different format:
// - system prompt is separate from messages
// - tool_use blocks instead of tool_calls
const systemMsg = messages.find(m => m.role === 'system');
const otherMsgs = messages.filter(m => m.role !== 'system');
const response = await this.client.messages.create({
model: 'claude-sonnet-4-20250514',
system: systemMsg?.content,
messages: this.convertMessages(otherMsgs),
tools: tools?.map(t => ({
name: t.name,
description: t.description,
input_schema: t.parameters,
})),
max_tokens: 4096,
});
return this.parseResponse(response);
}
}
各アダプターには、プラットフォームに依存しないインターフェイスとプロバイダー固有の API の間で 形式を変換するという固有のタスクがあります。
5. まとめ
| パターン | 役割 |
|---|---|
| アダプター | LLM プロバイダー間の API の違いを正規化する |
| 戦略 | TaskComplexityに応じてプロバイダーを選択 |
| 責任の連鎖 | プロバイダーに障害が発生した場合のフォールバック チェーン |
次の記事: ツール レジストリ — AI エージェント用のツールを登録して実行します。