5.6 KiB
OpenAI Responses: prompt_cache_key 送出によるキャッシュ有効化
背景
codex-oauth 経由 (ChatGPT backend) の OpenAI Responses API において、
プロンプトキャッシュが事実上効いていない。実セッションログ
(019de419-..., 171 turn / 累計入力 22.2M token) で cache_read_tokens
が全 turn 0、最新セッション (019de48f-..., 5 turn) でも 0。
直近のパース修正 (events.rs の ResponsesUsage に
input_tokens_details.cached_tokens を追加) で計測経路は復旧したが、
それでも 0 が観測される → server 側でそもそもキャッシュが効いていない。
原因は codex-rs の実装で確定:
// codex-rs/core/src/client.rs:853
let prompt_cache_key = Some(self.client.state.conversation_id.to_string());
ChatGPT backend では prompt_cache_key をリクエストに含めないと
プロンプトキャッシュが期待通りに動かない (org/project ハッシュが
別 conversation と衝突しやすく、ヒット率が著しく落ちる)。Codex は
conversation 単位の安定キーを毎リクエスト付けて、その名前空間内で
prefix をキャッシュさせている。
insomnia 側の ResponsesRequest には prompt_cache_key フィールドが
存在せず、Request 構造体にも会話/セッション単位の安定キー概念が無い。
このため codex-oauth で長尺の Run を走らせると毎 turn 全 prefix を
従量課金している。
方針
Request に provider-agnostic な cache_key: Option<String> を
足し、OpenAIResponsesScheme がそれを prompt_cache_key として
送る。pod 側は LLM 呼び出し時に SessionId をキーとして渡す。
他 scheme (Anthropic / Gemini / OpenAI Chat / Ollama) はフィールドを
無視する。既存の cache_anchor (Anthropic 用 prefix anchor) と
同じ「キャッシュヒントを Request に載せ、効く provider だけ拾う」
規約に揃える。
Fork との関係
session-store::fork / fork_at はいずれも新 SessionId を発行する。
本チケットでは 新 fork = 新 cache_key とする (素直に
SessionId.to_string() を渡す)。fork 直後の cache 明示ヒットは失われる
が、OpenAI Responses は automatic prefix matching も走るため完全に
冷えるわけではない。fork 越しに親の cache_key を継承して明示ヒットも
残す最適化は別チケットで検討する (本ticketの範囲外)。
要件
llm-worker 側
Requestにcache_key: Option<String>を追加 (types.rs:442のcache_anchorの隣)。doc コメントで「会話単位の安定キー。 prompt_cache_key として送られる (OpenAI Responses)。 prefix anchor を持たない provider は無視」を明記- ビルダ
Request::cache_key(impl Into<String>)を追加 OpenAIResponsesScheme::build_requestでrequest.cache_key.clone()をResponsesRequest::prompt_cache_keyにセットResponsesRequestにprompt_cache_key: Option<String>を追加 (#[serde(skip_serializing_if = "Option::is_none")])- 他 scheme (
anthropic,gemini,openai_chat) は touch しない (Request の新フィールドを未参照のまま残す)
pod 側
- LLM クライアントに渡す
Requestを組み立てる箇所でcache_key(session_id.to_string())を入れる。少なくとも以下:- 主 Run の LLM 呼び出し (
pod.rsの Run / Worker 経路) - compactor worker
- memory extract worker
- 主 Run の LLM 呼び出し (
SessionIdはSharedState::session_idから取得できる (shared_state.rs:21)- compactor / extract のように pod の中で派生する worker でも
同じ
session_idを使う。これにより pod 内のすべての LLM 呼び出しが同一 cache_key 名前空間で動き、prefix が共有される ところでヒットが期待できる
docs
docs/research/配下にopenai_responses_prompt_cache_key.md(仮) を追加し、「ChatGPT backend では prompt_cache_key 必須」 「codex-rs の挙動」「insomnia での Fork 方針」を残す。 既存のopenai_responses_max_output_tokens.mdと並びで置く
完了条件
Request::cache_key("abc")で組んだリクエストが、OpenAIResponsesScheme::build_requestでprompt_cache_key: "abc"を含む body を生成する (unit test)cache_key = Noneのときは body にprompt_cache_keyキーが 載らない (skip_serializing_if) (unit test)- pod の Run で codex-oauth + Responses を使ったとき、2 turn 目
以降の
cache_read_tokensが 0 でない (実セッションログで確認) cargo check/cargo testがllm-worker,provider,podで通る
範囲外
- Fork 越しのキャッシュ継承 (
forked_fromを辿って root の cache_key を継承する最適化)。別チケット - 公式 OpenAI Responses API (非 ChatGPT backend) での
prompt_cache_key必要性検証。少なくとも害は無いので両経路で 同じ値を送って良い - compaction で prefix が大きく書き換わる経路の cache_key 戦略 (compaction 後は prefix がほぼ別物なので、ヒット率を最大化する なら compaction 直後だけ別 key にする手もあるが、まずは単純に session_id 一本で動かす)
cache_anchor(Anthropic 用) とcache_key(Responses 用) の 統合。両者は別概念 (前者は prefix の境界 index、後者は 名前空間キー) なので並立させる
Review
- 状態: Approve
- レビュー詳細: ./responses-prompt-cache-key.review.md
- 日付: 2026-05-02