project-structure
リポジトリ: vercel/ai 分析日: 2026-02-20
概要
53 パッケージを擁する pnpm + Turborepo モノレポの分割戦略を分析した。このリポジトリは「インターフェース仕様」「共有ユーティリティ」「プロバイダー実装」「コア SDK」「フレームワークバインディング」という明確な依存層を持ち、30 以上の AI プロバイダーパッケージを同一構造で管理している。注目に値するのは、4 層アーキテクチャの厳格な依存方向制約、OpenAI Compatible という中間抽象層の導入、そしてビルド・テスト・リリース設定を tools/ ディレクトリで共有設定パッケージとして一元管理する手法である。
背景にある原則
依存方向の一方通行制約: 下位層(
@ai-sdk/provider)は上位層を一切参照しない。すべてのプロバイダーは@ai-sdk/providerと@ai-sdk/provider-utilsのみに依存し、コアパッケージaiや他のプロバイダーに依存しない。これにより、任意のプロバイダーを単体でインストールでき、不要な依存の連鎖を断てる。tsconfig.jsonのreferencesが依存グラフを正確に反映しており(packages/provider/tsconfig.json:referencesは空配列)、TypeScript の型チェックでも方向制約が保証される。同一カテゴリのパッケージは同一構造にする: 30 以上のプロバイダーパッケージが完全に同一の package.json 構造(scripts, exports, dependencies, devDependencies のパターン)を持つ。新規プロバイダー追加時のコストを最小化し、レビュー負荷を下げるため。
公開 API と内部 API の明示的分離: パッケージの
exportsフィールドで.(公開),./internal(パッケージ間共有),./test(テスト用)を分離する。これにより、セマンティックバージョニングの対象を公開 API のみに限定でき、内部リファクタリングの自由度を確保する。共有設定はワークスペース内パッケージとして配布する:
tools/tsconfig,tools/eslint-configをprivate: trueのワークスペースパッケージとして管理し、各パッケージからworkspace:*で参照する。npm で公開せずにモノレポ内で設定を一元管理できる。
実例と分析
4 層の依存グラフ
依存の流れは厳密に一方向である:
Layer 1: @ai-sdk/provider (インターフェース仕様のみ、外部依存は json-schema のみ)
↑
Layer 2: @ai-sdk/provider-utils (共有ユーティリティ、provider に依存)
↑
Layer 3: @ai-sdk/openai, @ai-sdk/anthropic, ... (各プロバイダー、provider + provider-utils に依存)
↑
Layer 4: ai (コア SDK、provider + provider-utils + gateway に依存)
↑
Layer 5: @ai-sdk/react, @ai-sdk/vue, @ai-sdk/svelte (フレームワーク、ai + provider-utils に依存)@ai-sdk/provider パッケージの tsconfig.json の references は空配列であり、最下層であることが型レベルでも保証されている(packages/provider/tsconfig.json:15)。
OpenAI Compatible 中間抽象層
OpenAI 互換 API を持つプロバイダー(xAI, Fireworks, DeepInfra, Cerebras 等 10 パッケージ)は @ai-sdk/openai-compatible を中間層として利用する。Fireworks の実装を見ると、OpenAICompatibleChatLanguageModel 等のクラスを直接インポートして使っている:
// packages/fireworks/src/fireworks-provider.ts:1-6
import {
OpenAICompatibleChatLanguageModel,
OpenAICompatibleCompletionLanguageModel,
OpenAICompatibleEmbeddingModel,
ProviderErrorStructure,
} from "@ai-sdk/openai-compatible";一方、OpenAI 自体や Anthropic のような独自 API を持つプロバイダーは @ai-sdk/provider + @ai-sdk/provider-utils のみに依存し、自前でモデルクラスを実装する。これにより「互換プロバイダーの追加は薄いラッパーで済む」一方で「独自 API プロバイダーの柔軟性は損なわない」二段構え構造になっている。
共有設定パッケージ tools/
tools/ ディレクトリには公開しない内部専用パッケージが 4 つある:
| パッケージ | 目的 |
|---|---|
@vercel/ai-tsconfig | TypeScript 設定の基盤(base.json, ts-library.json, react-library.json) |
eslint-config-vercel-ai | ESLint 共有設定 |
analyze-downloads | ダウンロード分析スクリプト |
generate-llms-txt | LLM 向けテキスト生成 |
各パッケージの tsconfig.json は @vercel/ai-tsconfig を extends する:
// packages/openai/tsconfig.json:2
{
"extends": "./node_modules/@vercel/ai-tsconfig/ts-library.json"
}tsconfig の 3 種類の分類(base.json → ts-library.json / react-library.json)はパッケージの性質に応じたプリセットとして機能する。
tsconfig.build.json パターン
すべてのパッケージで tsconfig.json(開発用、composite: true)と tsconfig.build.json(ビルド用、composite: false)を分離している:
// packages/openai/tsconfig.build.json
{
"extends": "./tsconfig.json",
"compilerOptions": {
"composite": false
}
}composite: true は TypeScript のプロジェクト参照(tsc --build)に必要だが、tsup によるバンドルビルドでは不要。tsup の --tsconfig tsconfig.build.json 指定でこれを使い分ける。
テストユーティリティの ./test サブパスエクスポート
ai と @ai-sdk/provider-utils の 2 パッケージだけが ./test サブパスをエクスポートする:
// packages/ai/test/index.ts:1-14
export {
convertArrayToAsyncIterable,
convertArrayToReadableStream,
// ...
} from "@ai-sdk/provider-utils/test";
export { MockLanguageModelV3 } from "../src/test/mock-language-model-v3";
export { MockProviderV3 } from "../src/test/mock-provider-v3";テストユーティリティを本体の公開 API から分離しつつ、ai/test として利用可能にする。test.d.ts ファイル(export * from './dist/test')がトップレベルのエイリアスとして機能する。
バージョン注入パターン
各パッケージは version.ts ファイルでビルド時に注入されたバージョン文字列をエクスポートする:
// packages/openai/src/version.ts
declare const __PACKAGE_VERSION__: string | undefined;
export const VERSION: string = typeof __PACKAGE_VERSION__ !== "undefined"
? __PACKAGE_VERSION__
: "0.0.0-test";tsup の define オプションで __PACKAGE_VERSION__ を package.json のバージョンに置換する(packages/ai/tsup.config.ts:14-17)。これにより、ランタイムで package.json を読み込むことなく User-Agent ヘッダーにバージョンを含められる。
prepack/postpack によるドキュメント同梱
プロバイダーパッケージは prepack で content/ ディレクトリからドキュメントをコピーし、postpack で削除する:
// packages/openai/package.json:29-30
"prepack": "mkdir -p docs && cp ../../content/providers/01-ai-sdk-providers/03-openai.mdx ./docs/",
"postpack": "del-cli docs"ドキュメントのソースオブトゥルースを content/ ディレクトリに一元管理しながら、npm パッケージにも同梱する戦略。
examples/ の Changesets 除外
examples は pnpm ワークスペースに含まれるが、Changesets によるバージョン管理からは除外する専用スクリプトがある(.github/scripts/cleanup-examples-changesets.mjs)。changeset version 実行後に例アプリの version を 0.0.0 にリセットし、CHANGELOG を削除する。
パターンカタログ
Layered Architecture (分類: アーキテクチャ)
- 解決する問題: 50 以上のパッケージ間の依存関係の複雑化
- 適用条件: パッケージ間に明確な抽象度の差がある場合
- コード例:
packages/provider/tsconfig.json:15(references: [])、packages/openai/package.json:46-48(provider + provider-utils のみ依存) - 注意点: 層を跨ぐ依存が入ると全体が崩壊するため、CI での依存方向チェックが望ましい
Adapter (分類: 構造)
- 解決する問題: OpenAI 互換 API を持つ多数のプロバイダーのコード重複
- 適用条件: 同じインターフェースの異なる実装が複数あり、共通部分が大きい場合
- コード例:
packages/fireworks/src/fireworks-provider.ts:1-6(OpenAICompatible 利用) - 注意点: 中間抽象層が肥大化すると独自拡張が困難になる
Abstract Factory (分類: 生成)
- 解決する問題: プロバイダーごとに異なるモデル群の統一的な生成
- 適用条件: 関連するオブジェクト群を一貫したインターフェースで生成する必要がある場合
- コード例:
packages/xai/src/xai-provider.ts:101-174(createXai 関数) - 注意点: ProviderV3 インターフェースが全プロバイダーの共通形状を規定
Good Patterns
- ワークスペースパッケージとしての共有設定:
tools/tsconfigを@vercel/ai-tsconfigとして各パッケージからworkspace:*で参照する。設定ファイルのコピペではなく、パッケージマネージャの依存解決で一元管理する。
// packages/openai/tsconfig.json
{
"extends": "./node_modules/@vercel/ai-tsconfig/ts-library.json"
}
// packages/react/tsconfig.json では react-library.json を extends する- サブパスエクスポートによる API 境界の制御:
exportsフィールドで.,./internal,./testを分離し、テストユーティリティや内部 API の公開範囲を制御する。
// packages/ai/package.json:43-61
"exports": {
".": { "types": "./dist/index.d.ts", "import": "./dist/index.mjs" },
"./internal": { "types": "./dist/internal/index.d.ts", "import": "./dist/internal/index.mjs" },
"./test": { "types": "./dist/test/index.d.ts", "import": "./dist/test/index.mjs" }
}- ビルド時バージョン注入:
version.ts+ tsup のdefineで、ランタイムに package.json を読まずにバージョン文字列を取得する。テスト環境ではフォールバック値'0.0.0-test'が使われる。
// packages/provider-utils/src/version.ts:1-6
declare const __PACKAGE_VERSION__: string | undefined;
export const VERSION: string = typeof __PACKAGE_VERSION__ !== "undefined"
? __PACKAGE_VERSION__
: "0.0.0-test";Anti-Patterns / 注意点
- 中間抽象層への暗黙的な結合:
@ai-sdk/openai-compatibleを使うプロバイダーは、OpenAI API の仕様変更に間接的に影響を受ける。中間層のインターフェースを安定させないと、一つの変更が 10 パッケージに波及する。
// Bad: 中間層の内部実装に直接依存する
import { OpenAICompatibleChatLanguageModel } from "@ai-sdk/openai-compatible";
// この具象クラスの constructor シグネチャが変わると全プロバイダーが壊れる
// Better: 中間層は安定したインターフェース(ProviderV3)のみを contract とし、
// 実装クラスの変更が利用側に波及しないようにする- pnpm-workspace.yaml での過剰なパス指定:
packages/rsc/tests/e2e/next-serverのようにテスト用の Next.js アプリが個別にワークスペースメンバーに追加されている。ワークスペースのフラットなpackages/*パターンが崩れ、見通しが悪くなる。
# pnpm-workspace.yaml
packages:
- 'packages/*'
- 'tools/*'
- 'examples/*'
- 'packages/rsc/tests/e2e/next-server' # 例外的追加導出ルール
[MUST]モノレポのパッケージ間依存は DAG(有向非巡回グラフ)とし、tsconfig の references や package.json の dependencies で方向制約を表現する- 根拠: vercel/ai では
@ai-sdk/providerの tsconfig references が空配列、プロバイダー群は provider + provider-utils のみに依存し、循環を型レベルで防止している
- 根拠: vercel/ai では
[MUST]同一カテゴリのパッケージ(プロバイダー等)は scripts, exports, dependencies のパターンを統一する- 根拠: 30 以上のプロバイダーが同一の package.json 構造(build, test, clean, lint スクリプト)を持ち、新規追加のテンプレート化とレビュー効率化を実現している
[SHOULD]モノレポ内の共有設定(tsconfig, eslint)は private ワークスペースパッケージとして配布し、各パッケージからworkspace:*で参照する- 根拠:
tools/tsconfigの@vercel/ai-tsconfigが 3 つのプリセット(base, ts-library, react-library)を提供し、50 以上のパッケージが extends で参照している
- 根拠:
[SHOULD]パッケージの公開 API、内部 API、テストユーティリティはexportsフィールドのサブパス(.,./internal,./test)で分離する- 根拠:
aiパッケージが 3 つのエクスポートパスを持ち、セマンティックバージョニングの対象を公開 API に限定しつつ、パッケージ間の内部共有を可能にしている
- 根拠:
[SHOULD]パッケージバージョンはビルド時にdefineで注入し、ランタイムで package.json を読み込まない- 根拠: 全プロバイダーの
version.tsが__PACKAGE_VERSION__を宣言し、tsup の define オプションで置換される。User-Agent ヘッダー生成等で使用
- 根拠: 全プロバイダーの
[SHOULD]共通パターンを持つ実装群(OpenAI 互換プロバイダー等)には中間抽象パッケージを設け、薄いラッパーで新規実装を追加できるようにする- 根拠:
@ai-sdk/openai-compatibleを使う 10 プロバイダーは、モデルクラスを自前実装する必要がなく、設定とエラー定義のみで済む
- 根拠:
[AVOID]ワークスペースの glob パターン(packages/*)に個別パスの例外を追加すること — ディレクトリ構成の見通しが悪化する- 根拠:
pnpm-workspace.yamlでpackages/rsc/tests/e2e/next-serverが例外的に追加されており、ワークスペース構成の一貫性が崩れている
- 根拠:
適用チェックリスト
- [ ] モノレポ内のパッケージ間依存が DAG になっているか(循環依存がないか)を確認する
- [ ] 同一カテゴリのパッケージ群が同一の package.json テンプレートに従っているか確認する
- [ ] 共有設定(tsconfig, eslint, prettier)が private ワークスペースパッケージとして管理されているか確認する
- [ ] 各パッケージの
exportsフィールドで公開 API と内部 API が分離されているか確認する - [ ] tsconfig の
referencesがパッケージの実際の依存グラフと一致しているか確認する - [ ] tsconfig.json(開発用、composite: true)と tsconfig.build.json(ビルド用、composite: false)が分離されているか確認する
- [ ] パッケージバージョンがランタイムで必要な場合、ビルド時注入パターンを使っているか確認する
- [ ] 共通パターンを持つ実装群に中間抽象パッケージの導入余地がないか検討する