Provider ソースコード分析
モジュール概要
Provider は OpenCode の モデル抽象化レイヤーおよびルーティングハブ であり、packages/opencode/src/provider/ に位置しています。TypeScript 版では、HTTP クライアントを直接構築するのではなく、Vercel AI SDK(ai パッケージ)を使用して 30 以上の LLM Provider と統一的にインターフェースします。上位モジュール(Agent、Session)は Provider.getLanguage() を通じて標準化された LanguageModel オブジェクトを取得し、その後 streamText / generateText を呼び出します。これにより、基盤となる API の差異から完全に分離されています。
コアとなる設計上の選択:
- Vercel AI SDK を統一アダプターレイヤーとして採用し、各 Provider ごとに HTTP プロトコルを手書きする作業を回避
- Effect Schema の branded 型(
ProviderID、ModelID)により、型レベルで異なるエンティティを区別 - models.dev をモデルメタデータの外部データソースとして使用し、実行時に動的にロード
CUSTOM_LOADERS辞書が各 Provider の特別なロジックを処理(Bedrock のリージョンプレフィックス、Copilot SDK 適応、Vertex 認証など)Instance.stateがプロセスレベルのシングルトンキャッシュを提供し、Provider の初期化は一度だけ実行
主要ファイル
| ファイル | 担当領域 |
|---|---|
src/provider/provider.ts | コア名前空間:状態管理、Provider ルーティング、SDK ファクトリ、モデル検索、CUSTOM_LOADERS |
src/provider/models.ts | models.dev データソースの読み込み:Zod スキーマ定義、JSON パース、定期リフレッシュ、スナップショットフォールバック |
src/provider/schema.ts | Effect Schema の branded 型 ProviderID / ModelID、Zod ブリッジを含む |
src/provider/transform.ts | リクエスト/レスポンス変換:メッセージ正規化、キャッシュマーカー、推論バリアント、temperature/topP 調整、エラーフォーマット |
src/provider/auth.ts | Provider 認証:OAuth フロー、API Key 管理、Plugin 認証統合 |
src/provider/error.ts | エラー分類:コンテキストオーバーフロー検出(12 以上の Provider エラーパターン)、API エラーパース、再試行可能性判定 |
src/provider/sdk/copilot/ | カスタム GitHub Copilot SDK アダプター(chat/ + responses/ サブディレクトリ、約 20 ファイル) |
sdk/copilot/ は公式の Vercel AI SDK パッケージに依存しない唯一の Provider です。これは、Copilot 固有のデバイス認証、トークンリフレッシュ、API 互換性の問題を処理する必要があるためです。README では「一時的なパッケージ」としてマークされており、将来的に公式 SDK に置き換えられる可能性があります。
型システム
Branded 型:ProviderID と ModelID
// schema.ts — Effect Schema branded types
const providerIdSchema = Schema.String.pipe(Schema.brand("ProviderID"))
export type ProviderID = typeof providerIdSchema.Type
export const ProviderID = providerIdSchema.pipe(
withStatics((schema) => ({
make: (id: string) => schema.makeUnsafe(id), // Runtime construction
zod: z.string().pipe(z.custom()), // Zod validation bridge
// Built-in constants
anthropic: schema.makeUnsafe("anthropic"),
openai: schema.makeUnsafe("openai"),
githubCopilot: schema.makeUnsafe("github-copilot"),
amazonBedrock: schema.makeUnsafe("amazon-bedrock"),
// ...
})),
)
Branded 型により、ProviderID と ModelID は コンパイル時には相互に置き換え不可能 になります。モデル ID を Provider ID を期待する関数に渡すことはできません。makeUnsafe は実行時に文字列から構築するために使用され(例:ユーザー設定のパース)、定数プロパティ(ProviderID.anthropic)は型安全な参照ポイントを提供します。
Provider.Model 完全なモデル定義
// provider.ts — Zod schema
export const Model = z.object({
id: ModelID.zod, // Branded type identifier
providerID: ProviderID.zod,
api: z.object({
id: z.string(), // Actual model name sent to the API
url: z.string(), // API base URL
npm: z.string(), // Vercel AI SDK package name (e.g. "@ai-sdk/anthropic")
}),
name: z.string(), // Human-readable name
family: z.string().optional(), // Model family (e.g. "claude", "gpt")
capabilities: z.object({
temperature: z.boolean(),
reasoning: z.boolean(), // Whether thinking/reasoning mode is supported
attachment: z.boolean(), // Whether attachments are supported
toolcall: z.boolean(), // Whether tool calls are supported
input: z.object({
text: z.boolean(), audio: z.boolean(),
image: z.boolean(), video: z.boolean(), pdf: z.boolean(),
}),
output: z.object({
text: z.boolean(), audio: z.boolean(),
image: z.boolean(), video: z.boolean(), pdf: z.boolean(),
}),
interleaved: z.union([ // Interleaved thinking mode
z.boolean(),
z.object({ field: z.enum(["reasoning_content", "reasoning_details"]) }),
]),
}),
cost: z.object({
input: z.number(), // $/M tokens (input)
output: z.number(), // $/M tokens (output)
cache: z.object({
read: z.number(), // Cache read unit price
write: z.number(), // Cache write unit price
}),
experimentalOver200K: z.object({ /* ... */ }).optional(), // Over 200K pricing
}),
limit: z.object({
context: z.number(), // Context window
input: z.number().optional(),
output: z.number(), // Maximum output tokens
}),
status: z.enum(["alpha", "beta", "deprecated", "active"]),
options: z.record(z.string(), z.any()), // Provider-specific options
headers: z.record(z.string(), z.string()), // Custom request headers
release_date: z.string(),
variants: z.record(/* reasoning effort variants */).optional(),
})
Model はモデルの能力だけでなく、完全なコスト情報 と 推論バリアント設定 も記述します。variants フィールドは ProviderTransform.variants() によりモデルと SDK パッケージ名に基づいて自動生成されます(例:Anthropic の high/max 思考予算、OpenAI の none/minimal/low/medium/high/xhigh 推論エフォート)。
Provider.Info
export const Info = z.object({
id: ProviderID.zod,
name: z.string(),
source: z.enum(["env", "config", "custom", "api"]), // Source tracking
env: z.string().array(), // Environment variable name list (e.g. ["ANTHROPIC_API_KEY"])
key: z.string().optional(), // Resolved API Key
options: z.record(z.string(), z.any()), // Provider-level options
models: z.record(z.string(), Model), // All models under this Provider
})
source フィールドは Provider 認証情報ソースの優先順位を追跡します:env(環境変数) > api(Auth ストア) > config(設定ファイル) > custom(Plugin/Loader)。
コアフロー
Provider 状態初期化
Instance.state() はプロセスレベルのシングルトンを作成し、以下の順序で利用可能な Provider を組み立てます:
1. Config.get() → ユーザー設定の読み取り
2. ModelsDev.get() → models.dev データの読み込み(キャッシュ → スナップショット → ネットワーク取得)
3. fromModelsDevProvider() → Provider.Info データベースへの変換
4. 環境変数スキャン → env フィールドの照合、source: "env" を設定
5. Auth.all() → 永続化された API Key / OAuth トークンの読み取り
6. Plugin.list() → Plugin 提供の認証の処理(例:Copilot デバイス認証)
7. CUSTOM_LOADERS → 各 Provider のカスタムロジックの実行
8. Config マージ → モデル定義、オプション、ホワイトリスト/ブラックリストの上書き
9. フィルタリング → 無効化された Provider、alpha(非実験モード)、モデルリストが空の非推奨 Provider の削除
初期化結果はメモリにキャッシュされ、その後の Provider.list() / Provider.getModel() の呼び出しはキャッシュから直接読み取ります。
SDK ファクトリと 30+ Provider ルーティング
getSDK() 関数は指定されたモデルの Vercel AI SDK Provider インスタンスを作成します:
// Built-in Provider mapping: 20 official SDK packages
const BUNDLED_PROVIDERS: Record<string, (...args: any[]) => SDK> = {
"@ai-sdk/anthropic": createAnthropic,
"@ai-sdk/openai": createOpenAI,
"@ai-sdk/google": createGoogleGenerativeAI,
"@ai-sdk/amazon-bedrock": createAmazonBedrock,
"@ai-sdk/azure": createAzure,
"@openrouter/ai-sdk-provider": createOpenRouter,
"@ai-sdk/github-copilot": createGitHubCopilotOpenAICompatible, // Custom adapter
// ... also Groq, Mistral, DeepInfra, Cerebras, Cohere, Together, Perplexity, Vercel, GitLab, etc.
}
ルーティングロジック:
BUNDLED_PROVIDERS[model.api.npm]を検索 — 見つかった場合、ファクトリ関数を直接呼び出し- 見つからない場合、
BunProc.install()を通じて npm パッケージを動的にインストールし、importする - 各インスタンスは
{ providerID, npm, options }のハッシュでキャッシュされ、同じ設定を再利用する
getLanguage() はさらに SDK インスタンスを LanguageModel に変換します:
// If this Provider has a custom modelLoader, use it; otherwise call sdk.languageModel()
const language = s.modelLoaders[model.providerID]
? await s.modelLoaders[model.providerID](sdk, model.api.id, provider.options)
: sdk.languageModel(model.api.id)
モデル発見:models.dev データソース
models.ts は 3 段階のフォールバックデータ読み込み戦略を実装しています:
優先順位:
1. ローカルキャッシュファイル (~/.cache/opencode/models.json)
2. ビルド時スナップショット (models-snapshot.ts、バイナリにバンドル)
3. リモート取得 (https://models.dev/api.json)
起動後、60 分ごとにバックグラウンドでリフレッシュします(setInterval + unref()、プロセスの終了をブロックしない)。モデルデータは ModelsDev.Model Zod スキーマで検証された後、Provider.Model に変換され、構造的一貫性が確保されます。
カスタム Provider ローダー(CUSTOM_LOADERS)
CUSTOM_LOADERS 辞書は特別な Provider のカスタムロジックを提供し、各ローダーは { autoload, getModel?, options?, vars? } を返します:
| Provider | 特別なロジック |
|---|---|
anthropic | ベータヘッダーの追加(claude-code-20250219、interleaved-thinking) |
openai | Chat の代わりに Responses API(sdk.responses(modelID))を使用 |
github-copilot | モデル ID に基づいて responses() または chat() を選択 |
amazon-bedrock | AWS 認証情報チェーン、リージョン推論、クロスリージョン推論プレフィックス(us./eu./apac.) |
google-vertex | Google ADC 認証、project/location パース、カスタム fetch インジェクション |
azure | 動的 baseURL テンプレート置換、resourceName 変数インジェクション |
gitlab | Agentic Chat API、カスタム User-Agent とフィーチャーフラグ |
cloudflare-ai-gateway | ai-gateway-provider パッケージの動的ロード、Unified API フォーマットルーティング |
opencode | API Key の利用可能性を検出し、Key がない場合は無料モデルのみ保持 |
Copilot カスタム SDK アダプター
provider/sdk/copilot/ ディレクトリにはスタンドアロンの Copilot SDK アダプターが含まれており、構造は以下の通りです:
sdk/copilot/
├── index.ts # createOpenaiCompatible factory function
├── copilot-provider.ts # Provider implementation
├── openai-compatible-error.ts # Error handling
├── chat/ # Chat Completions API adapter
└── responses/ # Responses API adapter
このアダプターは @ai-sdk/openai に依存せず、Copilot 固有のデバイス認証フロー(OAuth デバイスコードフロー)、トークンリフレッシュ、API 互換性処理を独自に実装しています。README では「一時的」とマークされており、Vercel AI SDK が公式の Copilot サポートを提供する際に削除される予定です。
カスタム fetch インジェクション
各 SDK インスタンスはインジェクションされたカスタム fetch と共に作成され、3 つの主要な機能を提供します:
- SSE タイムアウトラッパー(
wrapSSE):各チャンク読み取りにタイムアウトを設定、デフォルト 5 分。Provider のサイレント切断による接続の無限ハングを防止 - OpenAI リクエストボディ最適化:入力配列から
idフィールドを削除し、不要なネットワーク送信を削減 - Signal マージ:元の
signal+chunkAbort+timeoutをAbortSignal.any()で統一された中止シグナルに結合
// Signal merging illustration
const combined = AbortSignal.any([
originalSignal, // User cancellation
chunkAbort, // Chunk-level timeout
timeoutSignal, // Global timeout
])
この階層的な signal 設計により、ユーザー主導のキャンセルは即座に有効になり、個々のチャンクタイムアウトは再接続をトリガーし、グローバルタイムアウトが最終的なフォールバックとして機能します。
ProviderAuth OAuth フロー
ProviderAuth 名前空間は 2 つの認証タイプの完全なライフサイクルを処理します:
API Key 認証(key タイプ):
- 環境変数または設定ファイルから API Key を取得
provider.keyを直接設定し、source: "env"またはsource: "config"としてマーク
OAuth 認証(refresh タイプ):
- authorize フェーズ:Plugin フックから認証設定を取得(例:Copilot のデバイスコードフロー URL)
- callback フェーズ:
auto(自動コールバック)とcode(手動認証コード入力)のコールバック方式をサポート - 認証完了後、リフレッシュトークンが永続化され、自動リフレッシュによるセッション維持
// Authentication type determination
key → api type auth // Use API Key directly
refresh → oauth type auth // Use OAuth refresh token
モデルフィルタリングとホワイトリスト/ブラックリスト
Provider の初期化完了後、モデルリストは複数層のフィルタリングを経ます:
- autoload チェック:
CUSTOM_LOADERSでautoload: falseの Provider はスキップ(例:API Key がない Provider) - disable フラグ:
disable: trueとマークされたモデルは直接スキップ - 無料モデルフォールバック:API Key がない場合、無料とマークされたモデルのみ保持(例:
opencodeProvider の無料モデル) - SDK インスタンスキャッシュ:
{ providerID, npm, options }のハッシュでキャッシュ、同じ設定は同じインスタンスを再利用
LiteLLM プロキシ互換性
OpenCode は LiteLLM プロキシを検出し、メッセージフォーマットを自動的に適応させます:
- レスポンスヘッダーまたはリクエストパスの LiteLLM 識別子を検出
- メッセージ履歴にツール呼び出しが含まれているが、現在のメッセージにツール定義がない場合、ダミーツールをインジェクション
- これは、メッセージに tool_call ロールが含まれているのにリクエストにツールが含まれていない場合に LiteLLM がエラーになるためです。
この互換性処理により、OpenCode は LiteLLM プロキシを通じて様々なモデルに透過的にアクセスできます。
リクエスト変換レイヤー
ProviderTransform 名前空間は、メッセージが送信される前に Provider 固有の変換を処理します。変換パイプライン全体は wrapLanguageModel ミドルウェア内で呼び出されます:
wrapLanguageModel ミドルウェア
LLM.stream() は streamText() を呼び出す前に wrapLanguageModel() を通じてメッセージ変換ミドルウェアをインジェクションします:
model: wrapLanguageModel({
model: language,
middleware: [{
specificationVersion: "v3" as const,
async transformParams(args) {
if (args.type === "stream") {
args.params.prompt = ProviderTransform.message(args.params.prompt, input.model, options)
}
return args.params
},
}],
})
ProviderTransform.message() は 4 つの変換ステップを順番に実行します:unsupportedParts → normalizeMessages → applyCaching → providerOptions キーリマップ。このミドルウェア設計により、変換ロジックと SDK 呼び出しロジックが完全に分離されます。
メッセージ正規化(normalizeMessages)
- Anthropic:空のコンテンツメッセージをフィルタリング(API が空文字列を拒否するため)
- Claude モデル:ツール呼び出し ID の非英数字文字を
_に置換 - Mistral:ツール呼び出し ID を正確に 9 文字の英数字に正規化。ツールメッセージの直後にユーザーメッセージは不可(自動的に “Done.” アシスタントメッセージを挿入)
- インターリーブ思考モデル:推論部分をコンテンツから
providerOptions.openaiCompatible.reasoning_contentに移動
キャッシュマーカー(applyCaching)
Prompt Caching をサポートする Provider に自動的にキャッシュマーカーを追加します。キャッシュポリシーには明確なルールがあります:
- 最初の 2 つのシステムメッセージ + 最後の 2 つのメッセージ にキャッシュマーカーが付与
- 各 Provider は異なるフォーマットを使用:
// Anthropic
{ cacheControl: { type: "ephemeral" } }
// Amazon Bedrock
{ cachePoint: { type: "default" } }
Anthropic / OpenRouter / Bedrock など、Prompt Caching がサポートされていることが分かっている Provider に対してのみ有効です。他の Provider ではサイレントにスキップされます。
推論バリアント生成(variants)
model.api.npm と model.id に基づいて推論エフォートバリアントを自動生成します。各 Provider は異なるバリアントパラメータを持ちます:
- Anthropic:
{ thinking: { type: "enabled", budgetTokens } }(high: 16K、max: 32K) - OpenAI:
{ reasoningEffort, reasoningSummary: "auto" }(none から xhigh) - Google:
{ thinkingConfig: { thinkingBudget, includeThoughts } } - Bedrock:Anthropic モデルは
reasoningConfig.budgetTokens、Nova はmaxReasoningEffortを使用
モデル固有のチューニング
// transform.ts — temperature auto-adjusted based on model ID
function temperature(model: Provider.Model) {
if (id.includes("qwen")) return 0.55
if (id.includes("claude")) return undefined // Use default value
if (id.includes("gemini")) return 1.0
if (id.includes("kimi-k2")) return id.includes("thinking") ? 1.0 : 0.6
return undefined
}
エラー処理
コンテキストオーバーフロー検出
error.ts は 12 以上の Provider をカバーするオーバーフローパターンマッチングリストを維持しています:
const OVERFLOW_PATTERNS = [
/prompt is too long/i, // Anthropic
/input is too long for requested model/i, // Amazon Bedrock
/exceeds the context window/i, // OpenAI
/input token count.*exceeds the maximum/i, // Google Gemini
/maximum prompt length is \d+/i, // xAI Grok
/exceeds the limit of \d+/i, // GitHub Copilot
/context[_ ]length[_ ]exceeded/i, // Generic fallback
// ... more
]
エラーは 2 つのタイプに分類されます:
context_overflow:コンテキストがウィンドウ制限を超過、入力の切り詰めが必要api_error:一般的な API エラー、statusCode、isRetryable、responseBodyを含む
OpenAI Provider には特別な再試行ロジックがあります:HTTP 404 も再試行可能と見なされます(一部のモデルは最初のリクエストで 404 を返すが、実際には利用可能)。
カスタムエラータイプ
// provider.ts
ModelNotFoundError — Contains providerID, modelID, and fuzzysort fuzzy match suggestions
InitError — SDK initialization failure (package install/load error)
ProviderError.parseAPICallError() は上位レイヤー(Agent/Session)が使用する統一エラーパースのエントリポイントです。
SSE タイムアウト保護
wrapSSE() 関数は SSE ストリームにチャンクごとのタイムアウト(デフォルト 5 分)を追加し、ネットワーク切断時に接続が無限にハングするのを防止します:
function wrapSSE(res: Response, ms: number, ctl: AbortController) {
// Set timeout for each chunk read
// On timeout: abort connection + cancel reader
}
状態管理と Provider ホットスワップ
Provider の状態は Instance.state() を通じて管理されます。これは遅延初期化シングルトンファクトリであり、最初の呼び出しで初期化し、その後の呼び出しではキャッシュされた結果を返します。状態には以下が含まれます:
{
providers: Record<string, Provider.Info>, // All available Providers
models: Map<string, LanguageModel>, // Instantiated LanguageModel cache
sdk: Map<string, SDK>, // Instantiated SDK Provider cache
modelLoaders: Record<string, CustomModelLoader>, // Custom model loaders
varsLoaders: Record<string, CustomVarsLoader>, // Variable injectors (e.g. Azure resourceName)
}
モデルの切り替え時(ユーザーが異なるモデルを選択)、呼び出しチェーンは以下の通りです:
Provider.getModel(newProviderID, newModelID)— モデル情報の検索Provider.getLanguage(model)— LanguageModel インスタンスの取得または作成getSDK(model)— SDK Provider インスタンスの取得または作成- インスタンスは
{ providerID, npm, options }のハッシュでキャッシュ、同じ設定は再利用される
defaultModel() 関数は以下の優先順位でデフォルトモデルを選択します:
- 設定ファイルの
modelフィールド - 最後に使用されたモデル(
model.jsonのrecentリスト) - 優先順位順の最初の利用可能なモデル(
gpt-5>claude-sonnet-4>gemini-3-pro)
呼び出しチェーンの例
ユーザーがモデルを選択してからストリーミングレスポンスまで
ユーザーが "anthropic/claude-sonnet-4" を選択
│
▼
Provider.getModel("anthropic", "claude-sonnet-4")
│ ← state.providers.anthropic.models["claude-sonnet-4"] を検索
│ ← 見つからない場合? fuzzysort ファジーマッチで ModelNotFoundError をスロー
▼
Provider.getLanguage(model)
│ ← modelLoaders["anthropic"] を検索
│ ← カスタムローダーがある場合? loader(sdk, model.api.id, options) を呼び出し
│ ← ない場合 sdk.languageModel(model.api.id)
▼
getSDK(model)
│ ← ハッシュキー = hash({ providerID, npm: "@ai-sdk/anthropic", options }) を計算
│ ← キャッシュヒット? 直接返す
│ ← キャッシュミス: BUNDLED_PROVIDERS["@ai-sdk/anthropic"] → createAnthropic(options)
│ ← カスタム fetch をインジェクション(SSE タイムアウト + リクエストボディ最適化)
▼
Agent が streamText({ model: languageModel, messages, tools }) を呼び出し
│ ← Vercel AI SDK が実際の HTTP リクエストと SSE パースを処理
▼
ストリーミングレスポンスが上位レイヤーに返される
モデル切り替え時の状態更新
ユーザーが claude-sonnet-4 から gpt-5 に切り替え
│
▼
parseModel("openai/gpt-5") → { providerID: "openai", modelID: "gpt-5" }
│
▼
Provider.getModel("openai", "gpt-5")
│ ← state.providers["openai"] が存在するか?
│ ← state.providers["openai"].models["gpt-5"] が存在するか?
▼
Provider.getLanguage(model)
│ ← getSDK: BUNDLED_PROVIDERS["@ai-sdk/openai"] → createOpenAI(options)
│ ← modelLoaders["openai"]: sdk.responses("gpt-5") (Responses API)
▼
新しい LanguageModel インスタンスが state.models にキャッシュされる
設計上のトレードオフ
| 決定 | 理由 |
|---|---|
| 直接 HTTP の代わりに Vercel AI SDK | 30 以上の Provider 間のプロトコル差異を SDK が統一的に処理し、大量の HTTP クライアントコードの保守を回避。OpenCode はビジネスロジック(メッセージ変換、キャッシュ、エラー分類)に注力 |
| Effect Schema の branded 型 | コンパイル時の ProviderID / ModelID / string の混用を防止、ランタイムオーバーヘッドなし(branded 型は string に消去される) |
| models.dev 外部データソース | モデル情報(価格、コンテキストウィンドウ、機能)はコミュニティが管理し、OpenCode がハードコードする必要なし。3 段階フォールバックによりオフライン可用性を確保 |
| サブクラス継承の代わりに CUSTOM_LOADERS 辞書 | 各 Provider の異なるロジック(認証方式、API 選択、リージョン処理)は継承階層で表現するには差が大きすぎる。辞書 + 関数の方が柔軟 |
| カスタム Copilot SDK アダプター | Copilot のデバイス認証とトークンリフレッシュフローは標準 OAuth と差が大きく、公式 SDK がサポートしていないため、独立した実装が必要 |
| SSE チャンクごとのタイムアウト | Provider のサイレント切断による接続の無限ハングを防止。デフォルト 5 分、chunkTimeout オプションで調整可能 |
| fuzzysort ファジーマッチ提案 | モデル ID は頻繁にスペルミスやリネームされる。ファジーマッチはハードエラーの代わりに 3 つの提案を提供 |
| バンドルされていない SDK の BunProc 動的インストール | コミュニティ Provider(例:@mymediset/sap-ai-provider)をメインパッケージにバンドルする必要がなく、オンデマンドでインストール |
他のモジュールとの関係
- Agent:
Provider.getLanguage()を通じてLanguageModelを取得し、Vercel AI SDK のstreamText()/generateText()に渡して会話ループを駆動 - Config:Provider 初期化時に
config.model(デフォルトモデル)、config.provider(Provider 設定オーバーライド)、config.disabled_providers、config.enabled_providersを読み取り - Auth:
Auth.get(providerID)が永続化された API Key / OAuth トークンを読み取り。ProviderAuth名前空間が OAuth 認可フローを処理 - Plugin:
Plugin.list()が登録された認証方式(例:Copilot デバイス認証)を取得し、plugin.auth.loaderを通じて Provider オプションにインジェクション - Instance:
Instance.state()が Provider 状態のシングルトン管理を提供し、プロジェクトライフサイクルに紐付け - Transform:
streamText呼び出し前に、Agent がProviderTransform.message()を通じてメッセージを正規化し、キャッシュマーカーをインジェクション - Error:Agent が Vercel AI SDK の
APICallErrorをProviderError.parseAPICallError()で構造化されたParsedAPICallErrorに変換