AIアシスタントのチャットエンドポイントのAPI仕様です。

[Dynamic content unavailable in text export]

このreferenceはzudo-doc showcase repositoryのhost-ownedなlive実装を説明します。
`@takazudo/zudo-doc` package/scaffoldは、このhandler、`worker-entry.ts`、
`AiChatDailySpendCap`を出荷しません。host実装なしでlive modeを選ぶとpackage-owned routeは
HTTP 501を返します。downstream projectは同等のhandlerとWorker graphを用意する必要があります。



## エンドポイント

```
POST /api/ai-chat
Content-Type: application/json
```

### リクエストボディ

```ts
interface AiChatRequest {
  message: string;
  history?: ChatMessage[];
}

interface ChatMessage {
  role: "user" | "assistant";
  content: string;
}
```

| フィールド | 型              | 必須   | 説明                                                                                                                           |
| ---------- | --------------- | ------ | ------------------------------------------------------------------------------------------------------------------------------ |
| `message`  | `string`        | はい   | ユーザーの現在のメッセージ。空でないこと。                                                                                     |
| `history`  | `ChatMessage[]` | いいえ | 過去の会話メッセージ。不正な形式（非配列、50件超、コンテンツが8192文字超、またはインジェクション一致）の場合はHTTP 400で拒否。 |

### 成功レスポンス (200)

```ts
interface AiChatResponse {
  response: string;
}
```

`response`フィールドはアシスタントの返答をマークダウン文字列として含みます。

**例：**

```json
// Request
{
  "message": "How do I add a new page?",
  "history": []
}

// Response
{
  "response": "Create an MDX file in `src/content/docs/`:\n\n1. Add frontmatter with `title`\n2. Write your content in MDX\n3. The page appears in the sidebar automatically"
}
```

### エラーレスポンス (400 / 500)

```ts
interface AiChatErrorResponse {
  error: string;
}
```

| ステータス | 条件                                                            |
| ---------- | --------------------------------------------------------------- |
| 400        | 無効なJSONボディ                                                |
| 400        | `message`が空でない文字列ではない                               |
| 400        | `message`が4000文字の制限を超えている                           |
| 400        | 入力スクリーニング（プロンプトインジェクションガード）で拒否された |
| 400        | `history`の形式が不正（フィールド説明を参照）                   |
| 405        | リクエストメソッドが`POST`でも`OPTIONS`でもない                 |
| 415        | Content-Typeが`application/json`ではない                        |
| 429        | レート制限超過（`Retry-After`ヘッダーを含む）                   |
| 500        | Anthropic APIコール失敗                                         |

このエンドポイントは`POST`（チャット）と`OPTIONS`（CORSプリフライト）のみを受け付けます。それ以外のメソッドはすべて`{ "error": "Method not allowed" }`とともに`405`を返します。

### CORS

このエンドポイントは**オリジンごとの許可リスト**を使用します。`aiChatDemoMode`が`false`の場合、`Access-Control-Allow-Origin`は`aiChatAllowedOrigins`設定にリストされたリクエストオリジンに対してのみエコーされます。それ以外のオリジンにはallow-originヘッダーが返されず、ブラウザによってブロックされます。（デモモードでは後方互換性のため常に`*`が返されます。）これは**ワイルドカードCORS（`*`）を使用する[Search Worker](https://zudo-doc.takazudomodular.com/ja/docs/reference/search-worker)よりも意図的に厳格**です。各コールに実際のAnthropic APIコストが伴うAIチャットエンドポイントはオリジンでゲートしますが、検索はメーター課金のないオプトインサービスだからです。2つのエンドポイントが同じCORSポリシーを共有していると仮定しないでください。

## CF環境バインディング

| バインディング          | 種別          | 必須 | 説明                                                                       |
| ----------------------- | ------------- | ---- | -------------------------------------------------------------------------- |
| `ANTHROPIC_API_KEY`     | secret        | はい | Anthropic APIキー                                                          |
| `DOCS_SITE_URL`         | var           | はい | デプロイ済みのドキュメントサイトURL（`llms-full.txt`の取得に使用）         |
| `RATE_LIMIT`            | KV namespace  | はい | ソフトなIPごとのカウンタとprivacy-safeな結果監査recordを保存                |
| `AI_CHAT_DAILY_SPEND_CAP` | Durable Object namespace | `aiChatGlobalDailyLimit`が数値の場合 | 正確な有料呼び出し許可。クラス`AiChatDailySpendCap`、SQLite migration `v1-ai-chat-daily-spend-cap` |
| `RATE_LIMIT_PER_MINUTE` | var           | いいえ | IPあたりの1分間の最大リクエスト数（デフォルト`10`）                        |
| `RATE_LIMIT_PER_DAY`    | var           | いいえ | IPあたりの1日の最大リクエスト数（デフォルト`100`）                         |

## 設定

以下の`zfb.config.ts`内の`zudoDoc({...})`フィールドがエンドポイントの動作を制御します（上記のCF環境変数とは
別のもので、こちらはCloudflare側のランタイム設定です）。

| 設定                     | 型                  | デフォルト | 説明                                                                    |
| ------------------------ | ------------------- | ---------- | ----------------------------------------------------------------------- |
| `aiChatDemoMode`         | `boolean`           | `false`（ショーケース: `true`） | 固定返答でショートサーキット。APIキー、KV、Durable Object、provider fetchにアクセスしない |
| `aiChatAllowedOrigins`   | `string[]`          | `[]`       | CORSオリジン許可リスト（非デモ時のみ有効）。空配列 = 全クロスオリジンリクエストをブロック |
| `aiChatGlobalDailyLimit` | `number \| false`   | `false`    | UTC日ごとの正確なAnthropic fetch許可上限。`false` = 正確な上限なし       |

## セキュリティ

このエンドポイントは、レガシーのスタンドアロンWorkerから移植された多層的な防御を備えています：

- **ハードニングされたシステムプロンプト** — XMLタグでコンテキストを区切り、明示的なガードレールによってモデルが設定情報を漏洩したり、トピック外の指示に従ったりすることを防ぎます。さらに、過去の会話ターンはすべてクライアント由来の信頼できない入力として扱うようモデルに指示します（下記の **チャット履歴の信頼モデル** を参照）
- **入力スクリーニング** — 一般的なプロンプトインジェクションパターンを、どちらのリミッターやClaude APIよりも先に正規表現で検査します。その後、validation rejectionを返す前にIPごとのKVガードを実行するため、拒否された入力も近似的なIPごとのquotaを消費しますが、audit writeはそのガードより後ろに保たれます
- **IPごとのソフトガード** — `RATE_LIMIT` KVを使う近似的で結果整合な制限。`aiChatDemoMode` が `false` の場合は
  **フェイルクローズ**（KV障害時 → HTTP 429）。デモモードではフェイルオープン（デモのショートサーキットが先に実行されるため、実際にはレートリミッターには到達しない）
- **CORSオリジン許可リスト** — 非デモ時、`Access-Control-Allow-Origin` は `aiChatAllowedOrigins`
  に含まれるオリジンにのみエコーされます。リスト外のオリジンからのクロスオリジンリクエストはブラウザでブロックされます。デモモードは後方互換性のため常に `*` を送信します
- **正確な有料呼び出し許可** — UTC日ごとのSQLite `AiChatDailySpendCap`が、ちょうど1回のAnthropic
  fetch直前の許可判定を直列化します。名前空間は`AI_CHAT_DAILY_SPEND_CAP`、Worker migration tagは
  `v1-ai-chat-daily-spend-cap`、設定は`new_sqlite_classes = ["AiChatDailySpendCap"]`です。上限超過は
  次のUTC日まで429、binding/RPC/storage障害は500でフェイルクローズします。provider/network障害でも
  許可枠は返却せず、providerが確定した請求数ではありません
- **Privacy-safeな監査record** — `RATE_LIMIT` KVの`audit:`にはtimestamp、`completed`/`blocked`
  outcome、任意の限定されたblock-reason enumだけを保存します（7日のTTL、fire-and-forget）。prompt／response
  text、IP、IP hashは永続化しません
- **メッセージ長制限** — 4000文字を超えるメッセージはAPIに到達する前に拒否されます
- **`cf-connecting-ip`の注意点** — IPごとのレート制限にはこのヘッダーを使用しており、WorkerがCloudflareのネットワーク経由でデプロイされている場合にのみ信頼できます

## 運用ログ

Wranglerの`[observability] enabled = true`により、Workers Logsへ正確な上限の`admitted`、`denied`、
`failed_closed`をclosed-schemaオブジェクトとして記録します。各イベントには`utc_day`、
`configured_limit`と非機密な結果フィールドだけが含まれます。別の`per_ip_kv`イベントで、通常の
上限拒否とKV障害によるフェイルクローズを区別できます。prompt、response、生IP、IP hash、secret、
Durable Object名／ID、raw error textは記録しません。

## チャット履歴の信頼モデル

`history`配列は**クライアント由来かつステートレス**です。サーバーはセッション記録を一切保持しないため、`assistant`ロールのターンが実際に過去のモデル応答によって生成されたものかを検証できません。各エントリは依然としてハードニングされています。すなわち、`user`/`assistant`への厳格なロールホワイトリスト、上記のエントリ件数および1エントリあたりの長さ制限、そして紛れ込んだ余分なフィールドを取り除く`{ role, content }`への再構築です。`user`ロールのターンはインジェクションスクリーニングされますが、`assistant`ロールのターンは**されません**（本物のアシスタント応答はインジェクションのような文面を正当に引用しうるためです）。

ロールが検証不可能であるため、呼び出し元は敵対的な指示を含む`assistant`ターンを**偽造**してユーザーターンのスクリーニングを回避できます。この残存リスクは**意図的に受容**しています。本チャットはブラスト半径の小さいドキュメントアシスタントであり、システムプロンプトはすべての過去ターンを、自身のルールを上書きできない信頼できない入力として扱うようモデルに指示しているためです。堅牢な対策（サーバー発行の署名付き履歴）はシークレットのプロビジョニングとクライアント/サーバー間ペイロード契約の変更を必要とし、この機能には見合いません。決定の全記録は[issue #2036](https://github.com/zudolab/zudo-doc/issues/2036)を参照してください。

## ドキュメントコンテキスト

このエンドポイントは、[llms.txtインテグレーション](https://zudo-doc.takazudomodular.com/ja/docs/guides/llms-txt)で生成された`llms-full.txt`を`DOCS_SITE_URL`から取得し、CF Workersのアイソレートが生きている間メモリにキャッシュします（ベストエフォート、約1時間）。取得したコンテンツはシステムプロンプトに`<documentation>`XMLコンテキストとして含められます。