update: codexのキャッシュ利用が出来てなかった問題
This commit is contained in:
@@ -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"` だけのスキーマを
|
||||
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
|
||||
@@ -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,
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user