update: codexのキャッシュ利用が出来てなかった問題

This commit is contained in:
2026-05-02 03:23:44 +09:00
parent cb5cd1e3d1
commit 81ff4c6073
9 changed files with 379 additions and 1 deletions
@@ -210,6 +210,15 @@ struct ResponsesUsage {
output_tokens: Option<u64>,
#[serde(default)]
total_tokens: Option<u64>,
/// `input_tokens` の内訳。`cached_tokens` がプロンプトキャッシュヒット分。
#[serde(default)]
input_tokens_details: Option<InputTokensDetails>,
}
#[derive(Debug, Deserialize)]
struct InputTokensDetails {
#[serde(default)]
cached_tokens: Option<u64>,
}
#[derive(Debug, Deserialize)]
@@ -270,7 +279,10 @@ pub(crate) fn parse_sse(
total_tokens: usage.total_tokens.or_else(|| {
Some(usage.input_tokens.unwrap_or(0) + usage.output_tokens.unwrap_or(0))
}),
cache_read_input_tokens: None,
cache_read_input_tokens: usage
.input_tokens_details
.and_then(|d| d.cached_tokens),
// Responses API は cache 書き込みを別計上しない(input_tokens に含まれる)
cache_creation_input_tokens: None,
}));
}
@@ -554,9 +566,31 @@ mod tests {
assert_eq!(u.input_tokens, Some(10));
assert_eq!(u.output_tokens, Some(20));
assert_eq!(u.total_tokens, Some(30));
assert_eq!(u.cache_read_input_tokens, None);
assert_eq!(u.cache_creation_input_tokens, None);
}
}
#[test]
fn completed_extracts_cached_tokens_from_input_tokens_details() {
let data = r#"{"response":{"usage":{
"input_tokens":12345,
"input_tokens_details":{"cached_tokens":11000},
"output_tokens":50,
"total_tokens":12395
}}}"#;
let (events, _) = run("response.completed", data);
let Event::Usage(u) = &events[0] else {
panic!("expected usage")
};
assert_eq!(u.input_tokens, Some(12345));
assert_eq!(u.output_tokens, Some(50));
assert_eq!(u.total_tokens, Some(12395));
assert_eq!(u.cache_read_input_tokens, Some(11000));
// OpenAI Responses は cache 書き込みを別計上しない
assert_eq!(u.cache_creation_input_tokens, None);
}
#[test]
fn text_stream_start_delta_stop() {
let mut state = OpenAIResponsesState::default();
@@ -50,6 +50,11 @@ pub(crate) struct ResponsesRequest {
pub temperature: Option<f32>,
#[serde(skip_serializing_if = "Option::is_none")]
pub top_p: Option<f32>,
/// 会話単位の安定キー。ChatGPT backend (codex-oauth) は明示キーが
/// 無いとプロンプトキャッシュがほぼ効かない。pod 側は `SessionId`
/// を渡す。`Request::cache_key` が `None` のときはキー自体を送らない。
#[serde(skip_serializing_if = "Option::is_none")]
pub prompt_cache_key: Option<String>,
}
/// reasoning 制御。
@@ -220,6 +225,7 @@ impl OpenAIResponsesScheme {
} else {
None
},
prompt_cache_key: request.cache_key.clone(),
}
}
}
@@ -531,6 +537,29 @@ mod tests {
);
}
#[test]
fn prompt_cache_key_passed_through_when_set() {
let scheme = OpenAIResponsesScheme::new();
let req = Request::new().user("hi").cache_key("session-abc");
let body = scheme.build_request("gpt-5", &req, &cap_with_reasoning());
assert_eq!(body.prompt_cache_key.as_deref(), Some("session-abc"));
let json = serde_json::to_value(&body).unwrap();
assert_eq!(json["prompt_cache_key"], "session-abc");
}
#[test]
fn prompt_cache_key_omitted_when_none() {
let scheme = OpenAIResponsesScheme::new();
let req = Request::new().user("hi");
let body = scheme.build_request("gpt-5", &req, &cap_with_reasoning());
assert!(body.prompt_cache_key.is_none());
let json = serde_json::to_value(&body).unwrap();
assert!(
json.get("prompt_cache_key").is_none(),
"prompt_cache_key key must not appear in serialised body, got: {json}"
);
}
#[test]
fn tool_schema_without_properties_is_normalized() {
// schemars は引数なし struct から `type:"object"` だけのスキーマを
+16
View File
@@ -455,6 +455,14 @@ pub struct Request {
/// (Anthropic today) can place a long-lived cache breakpoint there.
/// Providers without prompt caching ignore the field.
pub cache_anchor: Option<usize>,
/// 会話単位の安定キー。`prompt_cache_key` として送られる
/// (OpenAI Responses)。ChatGPT backend (codex-oauth) は明示キーが
/// 無いと org/project ハッシュ衝突でプロンプトキャッシュが
/// ほぼヒットしないため、pod 側で `SessionId` を渡す運用を想定。
/// `cache_anchor` と違い名前空間キーであり、`prefix anchor` とは
/// 別の概念。`cache_anchor` を読まない provider と同じく、
/// `prompt_cache_key` を持たない provider は無視する。
pub cache_key: Option<String>,
}
impl Request {
@@ -534,6 +542,14 @@ impl Request {
self.config.stop_sequences.push(sequence.into());
self
}
/// Set the conversation cache key.
///
/// 詳細は [`Request::cache_key`] のフィールドコメント参照。
pub fn cache_key(mut self, key: impl Into<String>) -> Self {
self.cache_key = Some(key.into());
self
}
}
// ============================================================================
+16
View File
@@ -187,6 +187,10 @@ pub struct Worker<C: LlmClient, S: WorkerState = Mutable> {
/// Index of the last stable cache prefix item, set by higher layers.
/// Plumbed into [`Request::cache_anchor`] at request build time.
cache_anchor: Option<usize>,
/// Conversation-scoped cache key, set by higher layers. Plumbed into
/// [`Request::cache_key`] at request build time. Pod 側では
/// `SessionId` を渡す。
cache_key: Option<String>,
/// State marker
_state: PhantomData<S>,
}
@@ -392,6 +396,14 @@ impl<C: LlmClient, S: WorkerState> Worker<C, S> {
self.cache_anchor = anchor;
}
/// Set the conversation-scoped cache key. Plumbed into each outgoing
/// [`Request`] via [`Request::cache_key`] — caching-aware providers
/// that scope cache by an explicit key (OpenAI Responses) read it as
/// `prompt_cache_key`. Pass `None` to clear.
pub fn set_cache_key(&mut self, key: Option<String>) {
self.cache_key = key;
}
/// Get a mutable reference to the timeline (for additional handler registration)
pub fn timeline_mut(&mut self) -> &mut Timeline {
&mut self.timeline
@@ -585,6 +597,7 @@ impl<C: LlmClient, S: WorkerState> Worker<C, S> {
// if the prune projection trimmed items from the head — keep it
// in range).
request.cache_anchor = self.cache_anchor.filter(|&anchor| anchor < context.len());
request.cache_key = self.cache_key.clone();
request
}
@@ -1065,6 +1078,7 @@ impl<C: LlmClient> Worker<C, Mutable> {
prune_config: None,
savings_estimator: None,
cache_anchor: None,
cache_key: None,
_state: PhantomData,
}
}
@@ -1321,6 +1335,7 @@ impl<C: LlmClient> Worker<C, Mutable> {
prune_config: self.prune_config,
savings_estimator: self.savings_estimator,
cache_anchor: self.cache_anchor,
cache_key: self.cache_key,
_state: PhantomData,
}
}
@@ -1400,6 +1415,7 @@ impl<C: LlmClient> Worker<C, Locked> {
prune_config: self.prune_config,
savings_estimator: self.savings_estimator,
cache_anchor: self.cache_anchor,
cache_key: self.cache_key,
_state: PhantomData,
}
}