Merge remote-tracking branch 'origin/pr/614'

This commit is contained in:
elky
2026-06-02 22:10:41 +08:00
6 changed files with 1300 additions and 138 deletions
@@ -1,14 +1,21 @@
use std::collections::{BTreeMap, HashMap};
use std::fmt::Write as _;
use std::sync::{Mutex, OnceLock};
use std::time::{Duration, Instant};
use aether_runtime_state::{DataLayerError, RuntimeState};
use serde::{Deserialize, Serialize};
use serde_json::Value;
use sha2::{Digest, Sha256};
use tracing::warn;
use crate::clock::current_unix_ms;
const DEFAULT_CACHE_TTL: Duration = Duration::from_secs(300);
const ONE_HOUR_CACHE_TTL: Duration = Duration::from_secs(3600);
const MAX_ENTRIES: usize = 2048;
const PREFIX_LOOKBACK_LIMIT: usize = 10;
const KIRO_PROMPT_CACHE_INDEX_KEY: &str = "kiro:prompt-cache:index";
const PREFIX_LOOKBACK_WINDOW: usize = 20;
const TOKENS_PER_TOOL: u64 = 150;
const TOKENS_PER_MESSAGE: u64 = 4;
const INLINE_IMAGE_DATA_TOKEN_PLACEHOLDER: &str = "[inline-image-data]";
@@ -21,6 +28,7 @@ pub(crate) struct KiroPromptCacheProfile {
total_input_tokens: u64,
min_cacheable_tokens: u64,
breakpoints: Vec<KiroPromptCacheBreakpoint>,
match_candidates: Vec<KiroPromptCacheCandidate>,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
@@ -37,6 +45,12 @@ struct KiroPromptCacheEntry {
expires_at: Instant,
}
#[derive(Debug, Clone, Copy, Deserialize, Serialize)]
struct KiroPromptCacheRuntimeEntry {
token_count: u64,
ttl_secs: u64,
}
#[derive(Debug, Clone, Copy, Default, PartialEq, Eq)]
pub(crate) struct KiroPromptCacheUsage {
pub(crate) cache_creation_input_tokens: u64,
@@ -53,11 +67,10 @@ struct PendingBlock {
value: Value,
tokens: u64,
breakpoint_ttl: Option<Duration>,
is_message_end: bool,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
struct PrefixCandidate {
struct KiroPromptCacheCandidate {
fingerprint: [u8; 32],
cumulative_tokens: u64,
}
@@ -66,6 +79,230 @@ pub(crate) fn kiro_prompt_cache_tracker() -> &'static KiroPromptCacheTracker {
KIRO_PROMPT_CACHE_TRACKER.get_or_init(KiroPromptCacheTracker::default)
}
pub(crate) async fn compute_kiro_prompt_cache_usage(
runtime_state: &RuntimeState,
credential_id: String,
profile: &KiroPromptCacheProfile,
) -> KiroPromptCacheUsage {
match compute_kiro_prompt_cache_usage_with_runtime_state(
runtime_state,
credential_id.as_str(),
profile,
)
.await
{
Ok(usage) => usage,
Err(err) => {
warn!(
event_name = "kiro_simulated_cache_runtime_state_failed",
log_type = "event",
error = ?err,
"failed to update Kiro simulated cache runtime state; falling back to process-local tracker"
);
kiro_prompt_cache_tracker().compute_and_update(credential_id, profile)
}
}
}
async fn compute_kiro_prompt_cache_usage_with_runtime_state(
runtime_state: &RuntimeState,
credential_id: &str,
profile: &KiroPromptCacheProfile,
) -> Result<KiroPromptCacheUsage, DataLayerError> {
let last_breakpoint = match profile.breakpoints.last().copied() {
Some(last_breakpoint) => last_breakpoint,
None => return Ok(KiroPromptCacheUsage::default()),
};
let reversed_candidates = profile
.match_candidates
.iter()
.rev()
.copied()
.collect::<Vec<_>>();
let candidate_keys = reversed_candidates
.iter()
.map(|candidate| kiro_prompt_cache_runtime_key(credential_id, &candidate.fingerprint))
.collect::<Vec<_>>();
let candidate_values = runtime_state.kv_get_many(&candidate_keys).await?;
let mut existing_entries = HashMap::<String, KiroPromptCacheRuntimeEntry>::new();
let mut matched_tokens = 0u64;
let mut matched_refresh: Option<(String, KiroPromptCacheRuntimeEntry)> = None;
for ((candidate, key), value) in reversed_candidates
.iter()
.zip(candidate_keys.iter())
.zip(candidate_values)
{
let Some(entry) = value
.as_deref()
.and_then(parse_kiro_prompt_cache_runtime_entry)
else {
continue;
};
existing_entries.insert(key.clone(), entry);
if matched_tokens == 0 {
matched_tokens = entry
.token_count
.min(candidate.cumulative_tokens)
.min(profile.total_input_tokens);
matched_refresh = Some((key.clone(), entry));
}
}
if let Some((key, entry)) = matched_refresh {
store_kiro_prompt_cache_runtime_entry(runtime_state, key.as_str(), entry).await?;
}
let creation_tokens = last_breakpoint
.cumulative_tokens
.min(profile.total_input_tokens)
.saturating_sub(matched_tokens);
for breakpoint in &profile.breakpoints {
let key = kiro_prompt_cache_runtime_key(credential_id, &breakpoint.fingerprint);
let ttl_secs = breakpoint.ttl.as_secs().max(1);
let entry = existing_entries
.get(&key)
.copied()
.map(|existing| KiroPromptCacheRuntimeEntry {
token_count: existing.token_count.max(breakpoint.cumulative_tokens),
ttl_secs: existing.ttl_secs.max(ttl_secs),
})
.unwrap_or(KiroPromptCacheRuntimeEntry {
token_count: breakpoint.cumulative_tokens,
ttl_secs,
});
store_kiro_prompt_cache_runtime_entry(runtime_state, key.as_str(), entry).await?;
}
trim_kiro_prompt_cache_runtime_state(runtime_state, MAX_ENTRIES).await;
Ok(KiroPromptCacheUsage {
cache_creation_input_tokens: creation_tokens,
cache_read_input_tokens: matched_tokens,
})
}
async fn store_kiro_prompt_cache_runtime_entry(
runtime_state: &RuntimeState,
key: &str,
entry: KiroPromptCacheRuntimeEntry,
) -> Result<(), DataLayerError> {
let ttl = Duration::from_secs(entry.ttl_secs.max(1));
runtime_state
.kv_set(
key,
encode_kiro_prompt_cache_runtime_entry(entry),
Some(ttl),
)
.await?;
let expires_at_ms = current_unix_ms().saturating_add(entry.ttl_secs.saturating_mul(1000));
if let Err(err) = runtime_state
.score_set(KIRO_PROMPT_CACHE_INDEX_KEY, key, expires_at_ms as f64)
.await
{
warn!(
event_name = "kiro_simulated_cache_index_update_failed",
log_type = "event",
cache_key = %key,
error = ?err,
"failed to update Kiro simulated cache index; cache entry was persisted but cleanup may lag"
);
}
Ok(())
}
async fn trim_kiro_prompt_cache_runtime_state(runtime_state: &RuntimeState, max_entries: usize) {
if let Err(err) = runtime_state
.score_remove_by_score(KIRO_PROMPT_CACHE_INDEX_KEY, current_unix_ms() as f64)
.await
{
warn!(
event_name = "kiro_simulated_cache_index_expiry_trim_failed",
log_type = "event",
error = ?err,
"failed to trim expired Kiro simulated cache index entries"
);
return;
}
let Ok(index_len) = runtime_state.score_len(KIRO_PROMPT_CACHE_INDEX_KEY).await else {
return;
};
if index_len <= max_entries {
return;
}
let Ok(all_members) = runtime_state
.score_range_by_min(KIRO_PROMPT_CACHE_INDEX_KEY, 0.0)
.await
else {
return;
};
let trim_count = index_len.saturating_sub(max_entries);
if trim_count == 0 {
return;
}
let trimmed_members = all_members.into_iter().take(trim_count).collect::<Vec<_>>();
if let Err(err) = runtime_state.kv_delete_many(&trimmed_members).await {
warn!(
event_name = "kiro_simulated_cache_kv_trim_failed",
log_type = "event",
error = ?err,
trim_count,
"failed to delete trimmed Kiro simulated cache KV entries"
);
}
if let Err(err) = runtime_state
.score_remove_by_rank(KIRO_PROMPT_CACHE_INDEX_KEY, 0, trim_count as i64 - 1)
.await
{
warn!(
event_name = "kiro_simulated_cache_index_trim_failed",
log_type = "event",
error = ?err,
trim_count,
"failed to delete trimmed Kiro simulated cache index entries"
);
}
}
fn parse_kiro_prompt_cache_runtime_entry(value: &str) -> Option<KiroPromptCacheRuntimeEntry> {
serde_json::from_str::<KiroPromptCacheRuntimeEntry>(value)
.ok()
.filter(|entry| entry.token_count > 0 && entry.ttl_secs > 0)
}
fn encode_kiro_prompt_cache_runtime_entry(entry: KiroPromptCacheRuntimeEntry) -> String {
serde_json::to_string(&entry).unwrap_or_else(|_| {
format!(
r#"{{"token_count":{},"ttl_secs":{}}}"#,
entry.token_count, entry.ttl_secs
)
})
}
fn kiro_prompt_cache_runtime_key(credential_id: &str, fingerprint: &[u8; 32]) -> String {
let credential_hash: [u8; 32] = Sha256::digest(credential_id.as_bytes()).into();
format!(
"kiro:prompt-cache:{}:{}",
hex_digest(&credential_hash),
hex_digest(fingerprint)
)
}
fn hex_digest(bytes: &[u8]) -> String {
let mut output = String::with_capacity(bytes.len() * 2);
for byte in bytes {
let _ = write!(&mut output, "{byte:02x}");
}
output
}
pub(crate) fn build_kiro_prompt_cache_profile(
request_body: &Value,
total_input_tokens: u64,
@@ -75,7 +312,8 @@ pub(crate) fn build_kiro_prompt_cache_profile(
.and_then(Value::as_str)
.unwrap_or_default();
let flattened = flatten_cacheable_blocks(request_body);
if flattened.iter().all(|block| block.breakpoint_ttl.is_none()) {
let automatic_ttl = extract_cache_ttl(request_body);
if automatic_ttl.is_none() && flattened.iter().all(|block| block.breakpoint_ttl.is_none()) {
return None;
}
@@ -89,12 +327,15 @@ pub(crate) fn build_kiro_prompt_cache_profile(
prefix_hasher.update(prelude_bytes);
let mut cumulative_tokens = 0u64;
let mut active_ttl: Option<Duration> = None;
let mut breakpoints = Vec::new();
let mut seen_fingerprints = std::collections::BTreeSet::<[u8; 32]>::new();
let mut lookback_candidates = Vec::new();
let mut match_candidates = Vec::new();
let last_block_index = flattened.len().saturating_sub(1);
for block in flattened {
for (block_index, mut block) in flattened.into_iter().enumerate() {
if block.breakpoint_ttl.is_none() && block_index == last_block_index {
block.breakpoint_ttl = automatic_ttl;
}
cumulative_tokens = cumulative_tokens.saturating_add(block.tokens);
let block_bytes = serde_json::to_vec(&block.value).unwrap_or_default();
let block_hash: [u8; 32] = Sha256::digest(block_bytes).into();
@@ -105,13 +346,6 @@ pub(crate) fn build_kiro_prompt_cache_profile(
prefix_hasher.update(fingerprint);
if let Some(ttl) = block.breakpoint_ttl {
push_lookback_breakpoints(
&mut breakpoints,
&mut seen_fingerprints,
&lookback_candidates,
ttl,
);
active_ttl = Some(ttl);
push_breakpoint(
&mut breakpoints,
&mut seen_fingerprints,
@@ -120,18 +354,7 @@ pub(crate) fn build_kiro_prompt_cache_profile(
ttl,
);
}
if block.is_message_end {
if let Some(ttl) = active_ttl {
push_breakpoint(
&mut breakpoints,
&mut seen_fingerprints,
fingerprint,
cumulative_tokens,
ttl,
);
}
}
push_prefix_candidate(&mut lookback_candidates, fingerprint, cumulative_tokens);
push_match_candidate(&mut match_candidates, fingerprint, cumulative_tokens);
}
let min_cacheable_tokens = minimum_cacheable_tokens_for_model(model);
@@ -139,13 +362,54 @@ pub(crate) fn build_kiro_prompt_cache_profile(
.into_iter()
.filter(|breakpoint| breakpoint.cumulative_tokens >= min_cacheable_tokens)
.collect::<Vec<_>>();
(!cacheable_breakpoints.is_empty()).then_some(KiroPromptCacheProfile {
if cacheable_breakpoints.is_empty() {
return None;
}
let match_candidates = build_lookback_match_candidates(
&match_candidates,
&cacheable_breakpoints,
min_cacheable_tokens,
);
Some(KiroPromptCacheProfile {
total_input_tokens,
min_cacheable_tokens,
breakpoints: cacheable_breakpoints,
match_candidates,
})
}
fn build_lookback_match_candidates(
candidates: &[KiroPromptCacheCandidate],
breakpoints: &[KiroPromptCacheBreakpoint],
min_cacheable_tokens: u64,
) -> Vec<KiroPromptCacheCandidate> {
let mut out = Vec::new();
let mut seen_fingerprints = std::collections::BTreeSet::<[u8; 32]>::new();
for breakpoint in breakpoints {
let Some(index) = candidates
.iter()
.position(|candidate| candidate.fingerprint == breakpoint.fingerprint)
else {
continue;
};
let start = index
.saturating_add(1)
.saturating_sub(PREFIX_LOOKBACK_WINDOW);
for candidate in &candidates[start..=index] {
if candidate.cumulative_tokens < min_cacheable_tokens
|| candidate.cumulative_tokens > breakpoint.cumulative_tokens
|| !seen_fingerprints.insert(candidate.fingerprint)
{
continue;
}
out.push(*candidate);
}
}
out
}
pub(crate) fn kiro_simulated_cache_enabled_from_provider_config(config: Option<&Value>) -> bool {
config
.and_then(Value::as_object)
@@ -269,35 +533,15 @@ fn push_breakpoint(
}
}
fn push_lookback_breakpoints(
breakpoints: &mut Vec<KiroPromptCacheBreakpoint>,
seen_fingerprints: &mut std::collections::BTreeSet<[u8; 32]>,
candidates: &[PrefixCandidate],
ttl: Duration,
) {
for candidate in candidates {
push_breakpoint(
breakpoints,
seen_fingerprints,
candidate.fingerprint,
candidate.cumulative_tokens,
ttl,
);
}
}
fn push_prefix_candidate(
candidates: &mut Vec<PrefixCandidate>,
fn push_match_candidate(
candidates: &mut Vec<KiroPromptCacheCandidate>,
fingerprint: [u8; 32],
cumulative_tokens: u64,
) {
candidates.push(PrefixCandidate {
candidates.push(KiroPromptCacheCandidate {
fingerprint,
cumulative_tokens,
});
if candidates.len() > PREFIX_LOOKBACK_LIMIT {
candidates.remove(0);
}
}
fn flatten_cacheable_blocks(request_body: &Value) -> Vec<PendingBlock> {
@@ -316,7 +560,6 @@ fn flatten_cacheable_blocks(request_body: &Value) -> Vec<PendingBlock> {
tokens: TOKENS_PER_TOOL,
value,
breakpoint_ttl,
is_message_end: false,
});
}
}
@@ -337,7 +580,6 @@ fn flatten_cacheable_blocks(request_body: &Value) -> Vec<PendingBlock> {
tokens: count_system_block_tokens(item),
value,
breakpoint_ttl,
is_message_end: false,
});
}
}
@@ -351,7 +593,6 @@ fn flatten_cacheable_blocks(request_body: &Value) -> Vec<PendingBlock> {
tokens: count_text_tokens(text),
value,
breakpoint_ttl: None,
is_message_end: false,
});
}
other => {
@@ -364,7 +605,6 @@ fn flatten_cacheable_blocks(request_body: &Value) -> Vec<PendingBlock> {
tokens: count_system_block_tokens(other),
value,
breakpoint_ttl: None,
is_message_end: false,
});
}
}
@@ -376,11 +616,17 @@ fn flatten_cacheable_blocks(request_body: &Value) -> Vec<PendingBlock> {
.get("role")
.and_then(Value::as_str)
.unwrap_or_default();
let message_breakpoint_ttl = extract_cache_ttl(message);
match message.get("content") {
Some(Value::Array(items)) => {
let last_block_index = items.len().saturating_sub(1);
for (block_index, item) in items.iter().enumerate() {
let breakpoint_ttl = extract_cache_ttl(item);
let breakpoint_ttl =
extract_cache_ttl(item).or(if block_index == last_block_index {
message_breakpoint_ttl
} else {
None
});
let mut normalized = item.clone();
strip_cache_control(&mut normalized);
let value = canonicalize_json(serde_json::json!({
@@ -394,7 +640,6 @@ fn flatten_cacheable_blocks(request_body: &Value) -> Vec<PendingBlock> {
tokens: count_message_content_tokens(item),
value,
breakpoint_ttl,
is_message_end: block_index == last_block_index,
});
}
}
@@ -409,8 +654,7 @@ fn flatten_cacheable_blocks(request_body: &Value) -> Vec<PendingBlock> {
blocks.push(PendingBlock {
tokens: count_text_tokens(text),
value,
breakpoint_ttl: None,
is_message_end: true,
breakpoint_ttl: message_breakpoint_ttl,
});
}
Some(other) => {
@@ -424,8 +668,7 @@ fn flatten_cacheable_blocks(request_body: &Value) -> Vec<PendingBlock> {
blocks.push(PendingBlock {
tokens: count_message_content_tokens(other),
value,
breakpoint_ttl: None,
is_message_end: true,
breakpoint_ttl: message_breakpoint_ttl,
});
}
None => {}
@@ -634,20 +877,16 @@ impl KiroPromptCacheTracker {
};
let mut matched_tokens = 0;
for breakpoint in profile
.breakpoints
.iter()
.rev()
.take(PREFIX_LOOKBACK_LIMIT.saturating_add(1))
{
let key = (credential_id.clone(), breakpoint.fingerprint);
let Some(entry) = entries.get(&key) else {
for candidate in profile.match_candidates.iter().rev() {
let key = (credential_id.clone(), candidate.fingerprint);
let Some(entry) = entries.get_mut(&key) else {
continue;
};
if entry.expires_at > now {
entry.expires_at = entry.expires_at.max(now + entry.ttl);
matched_tokens = entry
.token_count
.min(breakpoint.cumulative_tokens)
.min(candidate.cumulative_tokens)
.min(profile.total_input_tokens);
break;
}
@@ -664,6 +903,7 @@ impl KiroPromptCacheTracker {
Some(existing) => {
existing.token_count = existing.token_count.max(breakpoint.cumulative_tokens);
existing.ttl = existing.ttl.max(breakpoint.ttl);
existing.expires_at = existing.expires_at.max(now + existing.ttl);
}
None => {
self.evict_to_capacity(&mut entries);
@@ -702,6 +942,7 @@ impl KiroPromptCacheTracker {
#[cfg(test)]
mod tests {
use super::*;
use aether_runtime_state::MemoryRuntimeStateConfig;
fn long_text(label: &str) -> String {
format!("{} {}", label, "cacheable prompt chunk ".repeat(300))
@@ -756,7 +997,138 @@ mod tests {
}
#[test]
fn tracker_supports_prefix_hits_without_extending_expiry() {
fn profile_reads_top_level_automatic_cache_control() {
let request = serde_json::json!({
"model": "claude-sonnet-4.6",
"cache_control": {"type": "ephemeral"},
"messages": [{
"role": "user",
"content": long_text("automatic cached turn")
}]
});
let profile =
build_kiro_prompt_cache_profile(&request, estimate_kiro_prompt_input_tokens(&request))
.expect("top-level cache_control should create an automatic cache profile");
let tracker = KiroPromptCacheTracker::default();
let usage = tracker.compute_and_update("cred".to_string(), &profile);
assert_eq!(profile.breakpoints.len(), 1);
assert!(usage.cache_creation_input_tokens > 0);
}
#[test]
fn profile_does_not_create_message_end_breakpoints_from_explicit_cache_control() {
let request = serde_json::json!({
"model": "claude-sonnet-4.6",
"system": [{
"type": "text",
"text": long_text("explicit cached system"),
"cache_control": {"type": "ephemeral"}
}],
"messages": [{
"role": "user",
"content": long_text("uncached later turn")
}]
});
let profile =
build_kiro_prompt_cache_profile(&request, estimate_kiro_prompt_input_tokens(&request))
.expect("explicit cache_control should create a cache profile");
assert_eq!(profile.breakpoints.len(), 1);
assert!(profile.breakpoints[0].cumulative_tokens < profile.total_input_tokens);
}
#[tokio::test]
async fn runtime_state_tracker_reads_cached_prefix_across_calls() {
let request = serde_json::json!({
"model": "claude-sonnet-4.6",
"system": [{
"type": "text",
"text": long_text("runtime shared system"),
"cache_control": {"type": "ephemeral"}
}],
"messages": [{"role": "user", "content": "reuse runtime cache"}]
});
let profile =
build_kiro_prompt_cache_profile(&request, estimate_kiro_prompt_input_tokens(&request))
.expect("cacheable request should create a cache profile");
let runtime = RuntimeState::memory(MemoryRuntimeStateConfig::default());
let first =
compute_kiro_prompt_cache_usage(&runtime, "runtime-cred".to_string(), &profile).await;
let second =
compute_kiro_prompt_cache_usage(&runtime, "runtime-cred".to_string(), &profile).await;
assert!(first.cache_creation_input_tokens > 0);
assert_eq!(first.cache_read_input_tokens, 0);
assert_eq!(second.cache_creation_input_tokens, 0);
assert!(second.cache_read_input_tokens > 0);
}
#[tokio::test]
async fn runtime_state_tracker_trims_oldest_entries_to_capacity() {
let runtime = RuntimeState::memory(MemoryRuntimeStateConfig::default());
let now_ms = current_unix_ms();
let keys = [
"kiro:prompt-cache:test-oldest".to_string(),
"kiro:prompt-cache:test-middle".to_string(),
"kiro:prompt-cache:test-newest".to_string(),
];
for (index, key) in keys.iter().enumerate() {
runtime
.kv_set(
key,
encode_kiro_prompt_cache_runtime_entry(KiroPromptCacheRuntimeEntry {
token_count: 100 + index as u64,
ttl_secs: 120,
}),
Some(Duration::from_secs(120)),
)
.await
.expect("cache entry should store");
runtime
.score_set(
KIRO_PROMPT_CACHE_INDEX_KEY,
key,
now_ms.saturating_add(60_000 + index as u64 * 1_000) as f64,
)
.await
.expect("cache index should store");
}
trim_kiro_prompt_cache_runtime_state(&runtime, 2).await;
assert_eq!(
runtime
.kv_get(&keys[0])
.await
.expect("oldest entry should read"),
None
);
assert!(runtime
.kv_get(&keys[1])
.await
.expect("middle entry should read")
.is_some());
assert!(runtime
.kv_get(&keys[2])
.await
.expect("newest entry should read")
.is_some());
assert_eq!(
runtime
.score_range_by_min(KIRO_PROMPT_CACHE_INDEX_KEY, 0.0)
.await
.expect("cache index should read"),
vec![keys[1].clone(), keys[2].clone()]
);
}
#[test]
fn tracker_refreshes_cached_prefix_ttl_on_read() {
let base = serde_json::json!({
"model": "claude-sonnet-4.6",
"system": [{
@@ -799,13 +1171,13 @@ mod tests {
);
assert!(hit.cache_read_input_tokens > 0);
let expired = tracker.compute_and_update_at(
let refreshed = tracker.compute_and_update_at(
"cred".to_string(),
&base_profile,
start + Duration::from_secs(301),
);
assert!(expired.cache_creation_input_tokens > 0);
assert_eq!(expired.cache_read_input_tokens, 0);
assert_eq!(refreshed.cache_creation_input_tokens, 0);
assert!(refreshed.cache_read_input_tokens > 0);
}
#[test]
@@ -867,6 +1239,146 @@ mod tests {
assert!(hit.cache_creation_input_tokens > 0);
}
#[test]
fn tracker_reads_cached_prefix_within_prompt_cache_lookback_window() {
let first = serde_json::json!({
"model": "claude-sonnet-4.6",
"messages": [{
"role": "user",
"content": [{
"type": "text",
"text": long_text("shared first turn"),
"cache_control": {"type": "ephemeral"}
}]
}]
});
let mut second_messages = vec![serde_json::json!({
"role": "user",
"content": [{
"type": "text",
"text": long_text("shared first turn")
}]
})];
for index in 0..12 {
second_messages.push(serde_json::json!({
"role": if index % 2 == 0 { "assistant" } else { "user" },
"content": format!("intermediate turn {index}")
}));
}
second_messages.push(serde_json::json!({
"role": "user",
"content": [{
"type": "text",
"text": long_text("new tail turn"),
"cache_control": {"type": "ephemeral"}
}]
}));
let second = serde_json::json!({
"model": "claude-sonnet-4.6",
"messages": second_messages
});
let first_profile =
build_kiro_prompt_cache_profile(&first, estimate_kiro_prompt_input_tokens(&first))
.expect("first request should be cacheable");
let second_profile =
build_kiro_prompt_cache_profile(&second, estimate_kiro_prompt_input_tokens(&second))
.expect("second request should be cacheable");
let tracker = KiroPromptCacheTracker::default();
let start = Instant::now();
let created = tracker.compute_and_update_at("cred".to_string(), &first_profile, start);
assert!(created.cache_creation_input_tokens > 0);
assert_eq!(created.cache_read_input_tokens, 0);
let hit = tracker.compute_and_update_at(
"cred".to_string(),
&second_profile,
start + Duration::from_secs(60),
);
assert!(hit.cache_read_input_tokens > 0);
assert!(hit.cache_creation_input_tokens > 0);
}
#[test]
fn tracker_does_not_read_cached_prefix_outside_prompt_cache_lookback_window() {
let first = serde_json::json!({
"model": "claude-sonnet-4.6",
"messages": [{
"role": "user",
"content": [{
"type": "text",
"text": long_text("shared first turn"),
"cache_control": {"type": "ephemeral"}
}]
}]
});
let mut second_messages = vec![serde_json::json!({
"role": "user",
"content": [{
"type": "text",
"text": long_text("shared first turn")
}]
})];
for index in 0..20 {
second_messages.push(serde_json::json!({
"role": if index % 2 == 0 { "assistant" } else { "user" },
"content": format!("intermediate turn {index}")
}));
}
second_messages.push(serde_json::json!({
"role": "user",
"content": [{
"type": "text",
"text": long_text("new tail turn"),
"cache_control": {"type": "ephemeral"}
}]
}));
let second = serde_json::json!({
"model": "claude-sonnet-4.6",
"messages": second_messages
});
let first_profile =
build_kiro_prompt_cache_profile(&first, estimate_kiro_prompt_input_tokens(&first))
.expect("first request should be cacheable");
let second_profile =
build_kiro_prompt_cache_profile(&second, estimate_kiro_prompt_input_tokens(&second))
.expect("second request should be cacheable");
let tracker = KiroPromptCacheTracker::default();
let start = Instant::now();
let created = tracker.compute_and_update_at("cred".to_string(), &first_profile, start);
assert!(created.cache_creation_input_tokens > 0);
assert_eq!(created.cache_read_input_tokens, 0);
let miss = tracker.compute_and_update_at(
"cred".to_string(),
&second_profile,
start + Duration::from_secs(60),
);
assert!(miss.cache_creation_input_tokens > 0);
assert_eq!(miss.cache_read_input_tokens, 0);
}
#[test]
fn profile_reads_message_level_cache_control() {
let request = serde_json::json!({
"model": "claude-sonnet-4.6",
"messages": [{
"role": "system",
"content": long_text("message level cached system"),
"cache_control": {"type": "ephemeral"}
}]
});
let profile =
build_kiro_prompt_cache_profile(&request, estimate_kiro_prompt_input_tokens(&request))
.expect("message-level cache_control should create a cache profile");
let tracker = KiroPromptCacheTracker::default();
let usage = tracker.compute_and_update("cred".to_string(), &profile);
assert!(usage.cache_creation_input_tokens > 0);
}
#[test]
fn billed_input_tokens_subtracts_cache_usage() {
assert_eq!(
@@ -15,8 +15,8 @@ use tracing::{debug, warn};
use uuid::Uuid;
use crate::execution_runtime::kiro_cache::{
billed_input_tokens, build_kiro_prompt_cache_profile, estimate_kiro_prompt_input_tokens,
kiro_prompt_cache_tracker, kiro_simulated_cache_enabled_from_provider_config,
billed_input_tokens, build_kiro_prompt_cache_profile, compute_kiro_prompt_cache_usage,
estimate_kiro_prompt_input_tokens, kiro_simulated_cache_enabled_from_provider_config,
KiroPromptCacheProfile, KiroPromptCacheUsage,
};
use crate::execution_runtime::ndjson::encode_stream_frame_ndjson;
@@ -160,14 +160,17 @@ pub(crate) async fn maybe_execute_kiro_web_search_stream(
let search_results = parse_mcp_search_results(&mcp_execution.result);
let cache_usage = if kiro_simulated_cache_enabled(state, plan).await {
request
.cache_profile
.as_ref()
.map(|profile| {
kiro_prompt_cache_tracker()
.compute_and_update(kiro_cache_credential_id(plan), profile)
})
.unwrap_or_default()
match request.cache_profile.as_ref() {
Some(profile) => {
compute_kiro_prompt_cache_usage(
state.runtime_state(),
kiro_cache_credential_id(plan),
profile,
)
.await
}
None => KiroPromptCacheUsage::default(),
}
} else {
KiroPromptCacheUsage::default()
};
@@ -65,7 +65,7 @@ use crate::execution_runtime::chatgpt_web_image::maybe_execute_chatgpt_web_image
use crate::execution_runtime::grok::maybe_execute_grok_stream;
use crate::execution_runtime::kiro_cache::{
billed_input_tokens as kiro_billed_input_tokens, build_kiro_prompt_cache_profile,
estimate_kiro_prompt_input_tokens, kiro_prompt_cache_tracker,
compute_kiro_prompt_cache_usage, estimate_kiro_prompt_input_tokens,
kiro_simulated_cache_enabled_from_provider_config,
kiro_simulated_cache_enabled_from_report_context, KiroPromptCacheUsage,
KIRO_SIMULATED_CACHE_ENABLED_CONTEXT_FIELD,
@@ -402,7 +402,8 @@ async fn seed_kiro_simulated_cache_enabled(
}
}
fn seed_kiro_report_context_prompt_cache_usage(
async fn seed_kiro_report_context_prompt_cache_usage(
state: &AppState,
plan: &ExecutionPlan,
report_context: &mut Option<Value>,
) {
@@ -450,8 +451,12 @@ fn seed_kiro_report_context_prompt_cache_usage(
return;
};
let cache_usage = kiro_prompt_cache_tracker()
.compute_and_update(kiro_stream_cache_credential_id(plan), &profile);
let cache_usage = compute_kiro_prompt_cache_usage(
state.runtime_state(),
kiro_stream_cache_credential_id(plan),
&profile,
)
.await;
if cache_usage.cache_creation_input_tokens == 0 && cache_usage.cache_read_input_tokens == 0 {
return;
}
@@ -494,7 +499,8 @@ fn kiro_cache_usage_from_report_context(report_context: &Value) -> Option<KiroPr
.and_then(kiro_cache_usage_from_context_object)
}
fn maybe_apply_kiro_prompt_cache_usage_to_stream_summary(
async fn maybe_apply_kiro_prompt_cache_usage_to_stream_summary(
state: &AppState,
plan: &ExecutionPlan,
report_context: Option<&Value>,
summary: &mut Option<ExecutionStreamTerminalSummary>,
@@ -572,8 +578,12 @@ fn maybe_apply_kiro_prompt_cache_usage_to_stream_summary(
return;
};
let cache_usage = kiro_prompt_cache_tracker()
.compute_and_update(kiro_stream_cache_credential_id(plan), &profile);
let cache_usage = compute_kiro_prompt_cache_usage(
state.runtime_state(),
kiro_stream_cache_credential_id(plan),
&profile,
)
.await;
if cache_usage.cache_creation_input_tokens == 0 && cache_usage.cache_read_input_tokens == 0 {
return;
}
@@ -1984,7 +1994,7 @@ async fn execute_stream_from_frame_stream(
seed_kiro_report_context_input_tokens(&plan, &mut report_context);
if status_code == 200 {
seed_kiro_simulated_cache_enabled(state, &plan, &mut report_context).await;
seed_kiro_report_context_prompt_cache_usage(&plan, &mut report_context);
seed_kiro_report_context_prompt_cache_usage(state, &plan, &mut report_context).await;
}
let mut buffered_frames = VecDeque::new();
let mut stream_terminal_summary: Option<ExecutionStreamTerminalSummary> = None;
@@ -3705,10 +3715,12 @@ async fn execute_stream_from_frame_stream(
}
maybe_apply_kiro_prompt_cache_usage_to_stream_summary(
&state_for_report,
&plan_for_report,
report_context_owned.as_ref(),
&mut stream_terminal_summary,
);
)
.await;
let requires_observed_terminal_event = stream_requires_observed_terminal_event(
plan_for_report.provider_api_format.as_str(),
stream_usage_report_context.as_ref(),
@@ -3952,6 +3964,10 @@ mod tests {
.with_execution_runtime_candidate(true)
}
fn test_state() -> AppState {
AppState::new().expect("gateway state should build")
}
#[test]
fn detects_client_visible_sse_terminal_events() {
assert!(stream_chunk_contains_sse_done(b"data: [DONE]\n\n"));
@@ -4136,8 +4152,8 @@ mod tests {
));
}
#[test]
fn kiro_stream_summary_applies_prompt_cache_usage_from_original_request() {
#[tokio::test]
async fn kiro_stream_summary_applies_prompt_cache_usage_from_original_request() {
let request_body = json!({
"model": "claude-opus-4-7",
"system": [
@@ -4185,6 +4201,7 @@ mod tests {
transport_profile: None,
timeouts: None,
};
let state = test_state();
let mut first_summary = Some(ExecutionStreamTerminalSummary {
standardized_usage: Some(StandardizedUsage {
@@ -4195,10 +4212,12 @@ mod tests {
..ExecutionStreamTerminalSummary::default()
});
maybe_apply_kiro_prompt_cache_usage_to_stream_summary(
&state,
&plan,
Some(&report_context),
&mut first_summary,
);
)
.await;
let first_usage = first_summary
.as_ref()
.and_then(|summary| summary.standardized_usage.as_ref())
@@ -4215,10 +4234,12 @@ mod tests {
..ExecutionStreamTerminalSummary::default()
});
maybe_apply_kiro_prompt_cache_usage_to_stream_summary(
&state,
&plan,
Some(&report_context),
&mut second_summary,
);
)
.await;
let second_usage = second_summary
.as_ref()
.and_then(|summary| summary.standardized_usage.as_ref())
@@ -4229,8 +4250,126 @@ mod tests {
assert_eq!(second_usage.output_tokens, 19);
}
#[test]
fn kiro_stream_summary_seeds_input_tokens_without_cache_control() {
#[tokio::test]
async fn kiro_stream_summary_reads_cached_prefix_within_prompt_cache_lookback_window() {
let first_request_body = json!({
"model": "claude-sonnet-4.6",
"messages": [{
"role": "user",
"content": [{
"type": "text",
"text": "shared first turn ".repeat(600),
"cache_control": {"type": "ephemeral"}
}]
}]
});
let mut second_messages = vec![json!({
"role": "user",
"content": [{
"type": "text",
"text": "shared first turn ".repeat(600)
}]
})];
for index in 0..12 {
second_messages.push(json!({
"role": if index % 2 == 0 { "assistant" } else { "user" },
"content": format!("intermediate stream turn {index}")
}));
}
second_messages.push(json!({
"role": "user",
"content": [{
"type": "text",
"text": "new tail turn ".repeat(600),
"cache_control": {"type": "ephemeral"}
}]
}));
let second_request_body = json!({
"model": "claude-sonnet-4.6",
"messages": second_messages
});
let plan = ExecutionPlan {
request_id: "req-kiro-cache-stream-long-tail".into(),
candidate_id: Some("cand-kiro-cache-stream-long-tail".into()),
provider_name: Some("Kiro".into()),
provider_id: "provider-kiro-cache-stream-long-tail".into(),
endpoint_id: "endpoint-kiro-cache-stream-long-tail".into(),
key_id: "key-kiro-cache-stream-long-tail".into(),
method: "POST".into(),
url: "https://q.us-east-1.amazonaws.com/generateAssistantResponse?beta=true".into(),
headers: BTreeMap::new(),
content_type: Some("application/json".into()),
content_encoding: None,
body: RequestBody::from_json(json!({"conversationState": {}})),
stream: true,
client_api_format: "claude:messages".into(),
provider_api_format: "claude:messages".into(),
model_name: Some("claude-sonnet-4.6".into()),
proxy: None,
transport_profile: None,
timeouts: None,
};
let first_report_context = json!({
"original_request_body": first_request_body,
"kiro_simulated_cache_enabled": true,
});
let second_report_context = json!({
"original_request_body": second_request_body,
"kiro_simulated_cache_enabled": true,
});
let state = test_state();
let mut first_summary = Some(ExecutionStreamTerminalSummary {
standardized_usage: Some(StandardizedUsage {
input_tokens: 4_000,
output_tokens: 17,
..StandardizedUsage::new()
}),
..ExecutionStreamTerminalSummary::default()
});
maybe_apply_kiro_prompt_cache_usage_to_stream_summary(
&state,
&plan,
Some(&first_report_context),
&mut first_summary,
)
.await;
let first_usage = first_summary
.as_ref()
.and_then(|summary| summary.standardized_usage.as_ref())
.expect("first usage should exist");
assert!(first_usage.cache_creation_tokens > 0);
assert_eq!(first_usage.cache_read_tokens, 0);
let mut second_summary = Some(ExecutionStreamTerminalSummary {
standardized_usage: Some(StandardizedUsage {
input_tokens: 8_000,
output_tokens: 19,
..StandardizedUsage::new()
}),
..ExecutionStreamTerminalSummary::default()
});
maybe_apply_kiro_prompt_cache_usage_to_stream_summary(
&state,
&plan,
Some(&second_report_context),
&mut second_summary,
)
.await;
let second_usage = second_summary
.as_ref()
.and_then(|summary| summary.standardized_usage.as_ref())
.expect("second usage should exist");
assert!(
second_usage.cache_read_tokens > 0,
"stream summary should reuse the far earlier cached prefix"
);
assert!(second_usage.cache_creation_tokens > 0);
assert_eq!(second_usage.output_tokens, 19);
}
#[tokio::test]
async fn kiro_stream_summary_seeds_input_tokens_without_cache_control() {
let request_body = json!({
"model": "claude-opus-4-7",
"system": [
@@ -4276,6 +4415,7 @@ mod tests {
transport_profile: None,
timeouts: None,
};
let state = test_state();
let mut summary = Some(ExecutionStreamTerminalSummary {
standardized_usage: Some(StandardizedUsage {
@@ -4287,10 +4427,12 @@ mod tests {
});
maybe_apply_kiro_prompt_cache_usage_to_stream_summary(
&state,
&plan,
Some(&report_context),
&mut summary,
);
)
.await;
let usage = summary
.as_ref()
@@ -4303,8 +4445,8 @@ mod tests {
assert_eq!(usage.output_tokens, 13);
}
#[test]
fn kiro_stream_summary_bills_existing_cache_usage_when_input_is_zero() {
#[tokio::test]
async fn kiro_stream_summary_bills_existing_cache_usage_when_input_is_zero() {
let request_body = json!({
"model": "claude-opus-4-7",
"system": [
@@ -4350,6 +4492,7 @@ mod tests {
transport_profile: None,
timeouts: None,
};
let state = test_state();
let mut summary = Some(ExecutionStreamTerminalSummary {
standardized_usage: Some(StandardizedUsage {
@@ -4362,10 +4505,12 @@ mod tests {
});
maybe_apply_kiro_prompt_cache_usage_to_stream_summary(
&state,
&plan,
Some(&report_context),
&mut summary,
);
)
.await;
let usage = summary
.as_ref()
@@ -4378,8 +4523,8 @@ mod tests {
assert_eq!(usage.output_tokens, 23);
}
#[test]
fn kiro_stream_summary_clears_cache_usage_when_simulated_cache_disabled() {
#[tokio::test]
async fn kiro_stream_summary_clears_cache_usage_when_simulated_cache_disabled() {
let request_body = json!({
"model": "claude-opus-4-7",
"system": [
@@ -4426,6 +4571,7 @@ mod tests {
transport_profile: None,
timeouts: None,
};
let state = test_state();
let mut summary = Some(ExecutionStreamTerminalSummary {
standardized_usage: Some(StandardizedUsage {
@@ -4439,10 +4585,12 @@ mod tests {
});
maybe_apply_kiro_prompt_cache_usage_to_stream_summary(
&state,
&plan,
Some(&report_context),
&mut summary,
);
)
.await;
let usage = summary
.as_ref()
@@ -4455,8 +4603,8 @@ mod tests {
assert_eq!(usage.output_tokens, 23);
}
#[test]
fn kiro_stream_summary_does_not_subtract_cache_from_already_billed_input() {
#[tokio::test]
async fn kiro_stream_summary_does_not_subtract_cache_from_already_billed_input() {
let request_body = json!({
"model": "claude-opus-4-7",
"messages": [
@@ -4504,6 +4652,7 @@ mod tests {
transport_profile: None,
timeouts: None,
};
let state = test_state();
let mut summary = Some(ExecutionStreamTerminalSummary {
standardized_usage: Some(StandardizedUsage {
@@ -4517,10 +4666,12 @@ mod tests {
});
maybe_apply_kiro_prompt_cache_usage_to_stream_summary(
&state,
&plan,
Some(&report_context),
&mut summary,
);
)
.await;
let usage = summary
.as_ref()
@@ -4640,9 +4791,11 @@ mod tests {
"original_request_body": request_body,
"kiro_simulated_cache_enabled": true,
}));
let state = AppState::new().expect("gateway state should build");
super::seed_kiro_report_context_input_tokens(&plan, &mut report_context);
super::seed_kiro_report_context_prompt_cache_usage(&plan, &mut report_context);
super::seed_kiro_report_context_prompt_cache_usage(&state, &plan, &mut report_context)
.await;
let context = report_context.as_ref().expect("context should exist");
assert!(context
@@ -4709,9 +4862,11 @@ mod tests {
let mut report_context = Some(json!({
"original_request_body": request_body,
}));
let state = AppState::new().expect("gateway state should build");
super::seed_kiro_report_context_input_tokens(&plan, &mut report_context);
super::seed_kiro_report_context_prompt_cache_usage(&plan, &mut report_context);
super::seed_kiro_report_context_prompt_cache_usage(&state, &plan, &mut report_context)
.await;
let context = report_context.as_ref().expect("context should exist");
assert!(context
@@ -41,6 +41,12 @@ use crate::clock::current_unix_ms as current_request_candidate_unix_ms;
use crate::control::GatewayControlDecision;
use crate::execution_runtime::chatgpt_web_image::maybe_execute_chatgpt_web_image_sync;
use crate::execution_runtime::grok::maybe_execute_grok_sync;
use crate::execution_runtime::kiro_cache::{
build_kiro_prompt_cache_profile, compute_kiro_prompt_cache_usage,
estimate_kiro_prompt_input_tokens, kiro_simulated_cache_enabled_from_provider_config,
kiro_simulated_cache_enabled_from_report_context, KiroPromptCacheUsage,
KIRO_SIMULATED_CACHE_ENABLED_CONTEXT_FIELD,
};
use crate::execution_runtime::oauth_retry::refresh_oauth_plan_auth_for_retry;
#[cfg(test)]
use crate::execution_runtime::remote_compat::post_sync_plan_to_remote_execution_runtime;
@@ -355,6 +361,180 @@ fn build_sync_report_payload(
}
}
fn seed_kiro_sync_report_context_input_tokens(
plan: &ExecutionPlan,
report_context: &mut Option<Value>,
) {
if !plan
.provider_name
.as_deref()
.is_some_and(|provider_name| provider_name.eq_ignore_ascii_case("Kiro"))
{
return;
}
let Some(context) = report_context.as_mut().and_then(Value::as_object_mut) else {
return;
};
if context
.get("input_tokens")
.and_then(Value::as_u64)
.is_some_and(|input_tokens| input_tokens > 0)
{
return;
}
let Some(original_request_body) = context.get("original_request_body").cloned() else {
return;
};
let estimated_input_tokens = estimate_kiro_prompt_input_tokens(&original_request_body);
context.insert(
"input_tokens".to_string(),
Value::from(estimated_input_tokens),
);
}
async fn seed_kiro_sync_simulated_cache_enabled(
state: &AppState,
plan: &ExecutionPlan,
report_context: &mut Option<Value>,
) {
if !plan
.provider_name
.as_deref()
.is_some_and(|provider_name| provider_name.eq_ignore_ascii_case("Kiro"))
{
return;
}
let enabled = match state
.read_provider_catalog_providers_by_ids(std::slice::from_ref(&plan.provider_id))
.await
{
Ok(providers) => providers
.iter()
.find(|provider| provider.id == plan.provider_id)
.filter(|provider| provider.provider_type.eq_ignore_ascii_case("kiro"))
.is_some_and(|provider| {
kiro_simulated_cache_enabled_from_provider_config(provider.config.as_ref())
}),
Err(err) => {
warn!(
event_name = "kiro_simulated_cache_config_read_failed",
log_type = "event",
request_id = %plan.request_id,
provider_id = %plan.provider_id,
error = ?err,
"failed to read Kiro simulated cache provider config; defaulting disabled"
);
false
}
};
let Some(context) = report_context.as_mut().and_then(Value::as_object_mut) else {
return;
};
if enabled {
context.insert(
KIRO_SIMULATED_CACHE_ENABLED_CONTEXT_FIELD.to_string(),
Value::Bool(true),
);
} else {
context.remove(KIRO_SIMULATED_CACHE_ENABLED_CONTEXT_FIELD);
}
}
async fn seed_kiro_sync_report_context_prompt_cache_usage(
state: &AppState,
plan: &ExecutionPlan,
report_context: &mut Option<Value>,
) {
if !plan
.provider_name
.as_deref()
.is_some_and(|provider_name| provider_name.eq_ignore_ascii_case("Kiro"))
{
return;
}
let simulated_cache_enabled =
kiro_simulated_cache_enabled_from_report_context(report_context.as_ref());
let Some(context) = report_context.as_mut().and_then(Value::as_object_mut) else {
return;
};
if context
.get("kiro_web_search_mcp")
.and_then(Value::as_bool)
.unwrap_or(false)
{
return;
}
if !simulated_cache_enabled {
return;
}
if kiro_cache_usage_from_context_object(context).is_some() {
return;
}
let Some(original_request_body) = context.get("original_request_body").cloned() else {
return;
};
let input_tokens = context
.get("input_tokens")
.and_then(Value::as_u64)
.filter(|value| *value > 0)
.unwrap_or_else(|| {
let estimated = estimate_kiro_prompt_input_tokens(&original_request_body);
context.insert("input_tokens".to_string(), Value::from(estimated));
estimated
});
let Some(profile) = build_kiro_prompt_cache_profile(&original_request_body, input_tokens)
else {
return;
};
let cache_usage = compute_kiro_prompt_cache_usage(
state.runtime_state(),
kiro_sync_cache_credential_id(plan),
&profile,
)
.await;
if cache_usage.cache_creation_input_tokens == 0 && cache_usage.cache_read_input_tokens == 0 {
return;
}
context.insert(
"cache_creation_input_tokens".to_string(),
Value::from(cache_usage.cache_creation_input_tokens),
);
context.insert(
"cache_read_input_tokens".to_string(),
Value::from(cache_usage.cache_read_input_tokens),
);
}
fn kiro_sync_cache_credential_id(plan: &ExecutionPlan) -> String {
format!("{}:{}:{}", plan.provider_id, plan.endpoint_id, plan.key_id)
}
fn kiro_cache_usage_from_context_object(
context: &serde_json::Map<String, Value>,
) -> Option<KiroPromptCacheUsage> {
let cache_creation_input_tokens = context
.get("cache_creation_input_tokens")
.and_then(Value::as_u64)
.unwrap_or(0);
let cache_read_input_tokens = context
.get("cache_read_input_tokens")
.and_then(Value::as_u64)
.unwrap_or(0);
(cache_creation_input_tokens > 0 || cache_read_input_tokens > 0).then_some(
KiroPromptCacheUsage {
cache_creation_input_tokens,
cache_read_input_tokens,
},
)
}
fn invalid_gemini_provider_success_message(
plan: &ExecutionPlan,
report_context: Option<&Value>,
@@ -1934,8 +2114,13 @@ async fn execute_execution_runtime_sync_impl(
}
let status_code = result.status_code;
let has_body_bytes = body_base64.is_some();
let report_context =
let mut report_context =
attach_provider_response_headers_to_report_context(report_context, &headers);
if (200..300).contains(&status_code) {
seed_kiro_sync_report_context_input_tokens(&plan, &mut report_context);
seed_kiro_sync_simulated_cache_enabled(state, &plan, &mut report_context).await;
seed_kiro_sync_report_context_prompt_cache_usage(state, &plan, &mut report_context).await;
}
let mut client_headers = headers.clone();
apply_endpoint_response_header_rules(state, &plan, &mut client_headers, body_json.as_ref())
.await?;
@@ -2504,6 +2689,45 @@ mod tests {
plan
}
fn test_kiro_sync_plan() -> ExecutionPlan {
ExecutionPlan {
request_id: "req-kiro-sync-cache-1".to_string(),
candidate_id: Some("candidate-kiro-sync-cache-1".to_string()),
provider_name: Some("Kiro".to_string()),
provider_id: "provider-kiro-sync-1".to_string(),
endpoint_id: "endpoint-kiro-sync-1".to_string(),
key_id: "key-kiro-sync-1".to_string(),
method: "POST".to_string(),
url: "https://kiro.example/generateAssistantResponse".to_string(),
headers: BTreeMap::new(),
content_type: Some("application/json".to_string()),
content_encoding: None,
body: aether_contracts::RequestBody::from_json(json!({
"model": "claude-sonnet-4",
"messages": [{"role": "user", "content": "hello kiro"}],
})),
stream: false,
client_api_format: "claude:messages".to_string(),
provider_api_format: "claude:messages".to_string(),
model_name: Some("claude-sonnet-4".to_string()),
proxy: None,
transport_profile: None,
timeouts: None,
}
}
fn test_kiro_sync_cacheable_request_body() -> serde_json::Value {
json!({
"model": "claude-sonnet-4",
"system": [{
"type": "text",
"text": format!("sync cacheable prompt {}", "cacheable prompt chunk ".repeat(300)),
"cache_control": {"type": "ephemeral"}
}],
"messages": [{"role": "user", "content": "reuse this Kiro prompt"}]
})
}
#[test]
fn invalid_gemini_provider_success_uses_plan_format_when_context_is_missing() {
let plan = test_gemini_chat_plan();
@@ -2659,6 +2883,68 @@ mod tests {
);
}
#[test]
fn kiro_sync_report_context_seeds_input_tokens_from_original_request_body() {
let plan = test_kiro_sync_plan();
let mut report_context = Some(json!({
"original_request_body": test_kiro_sync_cacheable_request_body(),
}));
seed_kiro_sync_report_context_input_tokens(&plan, &mut report_context);
assert!(report_context
.as_ref()
.and_then(|value| value.get("input_tokens"))
.and_then(Value::as_u64)
.is_some_and(|tokens| tokens > 0));
}
#[tokio::test]
async fn kiro_sync_report_context_applies_prompt_cache_usage_from_tracker() {
let state = AppState::new().expect("gateway state should build");
let plan = test_kiro_sync_plan();
let mut first_report_context = Some(json!({
"original_request_body": test_kiro_sync_cacheable_request_body(),
"kiro_simulated_cache_enabled": true,
}));
seed_kiro_sync_report_context_input_tokens(&plan, &mut first_report_context);
seed_kiro_sync_report_context_prompt_cache_usage(&state, &plan, &mut first_report_context)
.await;
let first_creation = first_report_context
.as_ref()
.and_then(|value| value.get("cache_creation_input_tokens"))
.and_then(Value::as_u64)
.unwrap_or_default();
let first_read = first_report_context
.as_ref()
.and_then(|value| value.get("cache_read_input_tokens"))
.and_then(Value::as_u64)
.unwrap_or_default();
assert!(first_creation > 0);
assert_eq!(first_read, 0);
let mut second_report_context = Some(json!({
"original_request_body": test_kiro_sync_cacheable_request_body(),
"kiro_simulated_cache_enabled": true,
}));
seed_kiro_sync_report_context_input_tokens(&plan, &mut second_report_context);
seed_kiro_sync_report_context_prompt_cache_usage(&state, &plan, &mut second_report_context)
.await;
let second_creation = second_report_context
.as_ref()
.and_then(|value| value.get("cache_creation_input_tokens"))
.and_then(Value::as_u64)
.unwrap_or_default();
let second_read = second_report_context
.as_ref()
.and_then(|value| value.get("cache_read_input_tokens"))
.and_then(Value::as_u64)
.unwrap_or_default();
assert_eq!(second_creation, 0);
assert!(second_read > 0);
}
#[tokio::test]
async fn json_whitespace_heartbeat_stream_prefixes_final_json() {
let (tx, rx) = mpsc::channel::<Result<Bytes, IoError>>(1);
@@ -8,6 +8,9 @@ use super::{
StoredProviderCatalogKey, StoredProviderCatalogProvider, StoredProviderModelMapping,
DEVELOPMENT_ENCRYPTION_KEY, TRACE_ID_HEADER,
};
use aether_data::repository::usage::InMemoryUsageReadRepository;
use aether_data_contracts::repository::usage::{StoredRequestUsageAudit, UsageReadRepository};
use aether_usage_runtime::UsageRuntimeConfig;
const KIRO_CLAUDE_CLI_SYNC_TEST_STACK_BYTES: usize = 16 * 1024 * 1024;
@@ -33,6 +36,30 @@ where
}
}
async fn wait_for_completed_usage<T>(repository: &T, request_id: &str) -> StoredRequestUsageAudit
where
T: UsageReadRepository + ?Sized,
{
let timeout = std::time::Duration::from_secs(60);
let deadline = tokio::time::Instant::now() + timeout;
loop {
if let Some(usage) = repository
.find_by_request_id(request_id)
.await
.expect("usage should read")
{
if usage.status == "completed" {
return usage;
}
}
assert!(
tokio::time::Instant::now() < deadline,
"usage {request_id} should complete within {timeout:?}"
);
tokio::time::sleep(std::time::Duration::from_millis(10)).await;
}
}
#[test]
fn gateway_executes_kiro_claude_cli_sync_via_local_provider_catalog_candidate() {
run_kiro_claude_cli_sync_test(
@@ -194,7 +221,7 @@ async fn gateway_executes_kiro_claude_cli_sync_via_local_provider_catalog_candid
Some(serde_json::json!({"url":"http://provider-proxy.internal:8080"})),
Some(20.0),
None,
None,
Some(serde_json::json!({"kiro": {"simulated_cache_enabled": true}})),
)
}
@@ -354,14 +381,15 @@ async fn gateway_executes_kiro_claude_cli_sync_via_local_provider_catalog_candid
let raw_body = to_bytes(body, usize::MAX).await.expect("body should read");
let payload: serde_json::Value =
serde_json::from_slice(&raw_body).expect("execution runtime payload should parse");
let trace_id = parts
.headers
.get(TRACE_ID_HEADER)
.and_then(|value| value.to_str().ok())
.unwrap_or_default()
.to_string();
*seen_execution_runtime_inner.lock().expect("mutex should lock") =
Some(SeenExecutionRuntimeSyncRequest {
trace_id: parts
.headers
.get(TRACE_ID_HEADER)
.and_then(|value| value.to_str().ok())
.unwrap_or_default()
.to_string(),
trace_id: trace_id.clone(),
url: payload
.get("url")
.and_then(|value| value.as_str())
@@ -453,7 +481,7 @@ async fn gateway_executes_kiro_claude_cli_sync_via_local_provider_catalog_candid
.concat();
Json(json!({
"request_id": "trace-kiro-cli-local-sync-123",
"request_id": trace_id,
"status_code": 200,
"headers": {
"content-type": "application/vnd.amazon.eventstream"
@@ -481,6 +509,7 @@ async fn gateway_executes_kiro_claude_cli_sync_via_local_provider_catalog_candid
sample_candidate_row(),
]));
let request_candidate_repository = Arc::new(InMemoryRequestCandidateRepository::default());
let usage_repository = Arc::new(InMemoryUsageReadRepository::default());
let provider_catalog_repository = Arc::new(InMemoryProviderCatalogReadRepository::seed(
vec![sample_provider_catalog_provider()],
vec![sample_provider_catalog_endpoint()],
@@ -491,34 +520,51 @@ async fn gateway_executes_kiro_claude_cli_sync_via_local_provider_catalog_candid
let (execution_runtime_url, execution_runtime_handle) = start_server(execution_runtime).await;
let gateway_state = build_state_with_execution_runtime_override(execution_runtime_url.clone())
.with_data_state_for_tests(
crate::data::GatewayDataState::with_auth_candidate_selection_provider_catalog_and_request_candidate_repository_for_tests(
crate::data::GatewayDataState::with_auth_candidate_selection_provider_catalog_request_candidates_and_usage_for_tests(
auth_repository,
candidate_selection_repository,
provider_catalog_repository,
Arc::clone(&request_candidate_repository),
Arc::clone(&usage_repository),
DEVELOPMENT_ENCRYPTION_KEY,
),
);
)
.with_usage_runtime_for_tests(UsageRuntimeConfig {
enabled: true,
..UsageRuntimeConfig::default()
});
let gateway = build_router_with_state(gateway_state);
let (gateway_url, gateway_handle) = start_server(gateway).await;
let response = reqwest::Client::new()
.post(format!("{gateway_url}/v1/messages"))
.header(http::header::CONTENT_TYPE, "application/json")
.header(
http::header::AUTHORIZATION,
"Bearer sk-client-kiro-cli-local-sync",
)
.header(TRACE_ID_HEADER, "trace-kiro-cli-local-sync-123")
.body(
"{\"model\":\"claude-sonnet-4\",\"messages\":[{\"role\":\"user\",\"content\":\"hello kiro\"}],\"thinking\":{\"type\":\"enabled\",\"budget_tokens\":64}}",
)
.send()
.await
.expect("request should succeed");
async fn send_kiro_request(
gateway_url: &str,
trace_id: &str,
body: String,
) -> (StatusCode, String) {
let response = reqwest::Client::new()
.post(format!("{gateway_url}/v1/messages"))
.header(http::header::CONTENT_TYPE, "application/json")
.header(
http::header::AUTHORIZATION,
"Bearer sk-client-kiro-cli-local-sync",
)
.header(TRACE_ID_HEADER, trace_id)
.body(body)
.send()
.await
.expect("request should succeed");
let status = response.status();
let response_body = response.text().await.expect("body should read");
let status = response.status();
let response_body = response.text().await.expect("body should read");
(status, response_body)
}
let (status, response_body) = send_kiro_request(
&gateway_url,
"trace-kiro-cli-local-sync-123",
"{\"model\":\"claude-sonnet-4\",\"messages\":[{\"role\":\"user\",\"content\":\"hello kiro\"}],\"thinking\":{\"type\":\"enabled\",\"budget_tokens\":64}}".to_string(),
)
.await;
assert!(
status == StatusCode::OK,
"unexpected status={status} body={response_body} decision_hits={} plan_hits={} public_hits={}",
@@ -598,6 +644,58 @@ async fn gateway_executes_kiro_claude_cli_sync_via_local_provider_catalog_candid
"report-sync should stay local when request candidate persistence is available"
);
let cacheable_request_body = serde_json::json!({
"model": "claude-sonnet-4",
"system": [{
"type": "text",
"text": format!("sync cacheable prompt {}", "cacheable prompt chunk ".repeat(300)),
"cache_control": {"type": "ephemeral"}
}],
"messages": [{"role": "user", "content": "reuse this Kiro prompt"}]
})
.to_string();
let (first_cache_status, first_cache_body) = send_kiro_request(
&gateway_url,
"trace-kiro-cli-local-sync-cache-1",
cacheable_request_body.clone(),
)
.await;
assert!(
first_cache_status == StatusCode::OK,
"unexpected first cache status={first_cache_status} body={first_cache_body}"
);
let first_usage = wait_for_completed_usage(
usage_repository.as_ref(),
"trace-kiro-cli-local-sync-cache-1",
)
.await;
assert!(
first_usage.cache_creation_input_tokens > 0,
"first Kiro sync cacheable request should create simulated cache"
);
assert_eq!(first_usage.cache_read_input_tokens, 0);
let (second_cache_status, second_cache_body) = send_kiro_request(
&gateway_url,
"trace-kiro-cli-local-sync-cache-2",
cacheable_request_body,
)
.await;
assert!(
second_cache_status == StatusCode::OK,
"unexpected second cache status={second_cache_status} body={second_cache_body}"
);
let second_usage = wait_for_completed_usage(
usage_repository.as_ref(),
"trace-kiro-cli-local-sync-cache-2",
)
.await;
assert!(
second_usage.cache_read_input_tokens > 0,
"second Kiro sync cacheable request should read simulated cache"
);
assert_eq!(second_usage.cache_creation_input_tokens, 0);
assert_eq!(*decision_hits.lock().expect("mutex should lock"), 0);
assert_eq!(*plan_hits.lock().expect("mutex should lock"), 0);
assert_eq!(*public_hits.lock().expect("mutex should lock"), 0);
+109 -1
View File
@@ -146,6 +146,7 @@ pub struct SyncTerminalUsagePayloadSeed {
pub provider_response_body_state: Option<UsageBodyCaptureState>,
pub client_response: Option<Value>,
pub client_response_body_state: Option<UsageBodyCaptureState>,
pub standardized_usage: Option<StandardizedUsage>,
pub capture_metadata: Option<Value>,
}
@@ -766,6 +767,7 @@ pub fn build_sync_terminal_usage_payload_seed(
.or_else(|| headers_to_json(&payload.headers));
let client_response_headers = context_usage_value(context, "client_response_headers")
.or_else(|| headers_to_json(&payload.headers));
let standardized_usage = kiro_simulated_cache_standardized_usage_from_context(context);
SyncTerminalUsagePayloadSeed {
report_kind: payload.report_kind.clone(),
status_code: payload.status_code,
@@ -780,6 +782,7 @@ pub fn build_sync_terminal_usage_payload_seed(
provider_response_body_state,
client_response,
client_response_body_state,
standardized_usage,
capture_metadata: build_payload_body_capture_metadata(
payload.body_base64.as_deref(),
None,
@@ -856,11 +859,14 @@ pub fn build_sync_terminal_usage_seed(
provider_response_body_state,
client_response,
client_response_body_state,
standardized_usage,
capture_metadata,
} = payload_seed;
let standardized_usage = provider_response_full
let derived_standardized_usage = provider_response_full
.as_ref()
.map(|response| map_usage_from_response(response, context_seed.provider_contract.as_str()));
let standardized_usage =
merge_standardized_usage_with_context_cache(standardized_usage, derived_standardized_usage);
let terminal_state = infer_sync_terminal_state(
report_kind.as_str(),
status_code,
@@ -914,6 +920,25 @@ pub fn build_sync_terminal_usage_seed(
}
}
fn merge_standardized_usage_with_context_cache(
context_usage: Option<StandardizedUsage>,
derived_usage: Option<StandardizedUsage>,
) -> Option<StandardizedUsage> {
let Some(context_usage) = context_usage else {
return derived_usage;
};
let mut usage = derived_usage.unwrap_or_default();
usage.input_tokens = context_usage.input_tokens;
if context_usage.cache_creation_tokens > 0 {
usage.cache_creation_tokens = context_usage.cache_creation_tokens;
}
if context_usage.cache_read_tokens > 0 {
usage.cache_read_tokens = context_usage.cache_read_tokens;
}
Some(usage)
}
pub fn build_stream_terminal_usage_seed(
context_seed: TerminalUsageContextSeed,
payload_seed: StreamTerminalUsagePayloadSeed,
@@ -1760,6 +1785,31 @@ fn context_body_value(context: Option<&Map<String, Value>>, key: &str) -> Option
}
}
fn kiro_simulated_cache_standardized_usage_from_context(
context: Option<&Map<String, Value>>,
) -> Option<StandardizedUsage> {
let enabled = context_bool(context, "kiro_simulated_cache_enabled").unwrap_or(false);
if !enabled {
return None;
}
let input_tokens = context_u64(context, "input_tokens")?;
let cache_creation_tokens = context_u64(context, "cache_creation_input_tokens").unwrap_or(0);
let cache_read_tokens = context_u64(context, "cache_read_input_tokens").unwrap_or(0);
if cache_creation_tokens == 0 && cache_read_tokens == 0 {
return None;
}
let billed_input_tokens = input_tokens
.saturating_sub(cache_creation_tokens)
.saturating_sub(cache_read_tokens);
let mut usage = StandardizedUsage::new();
usage.input_tokens = billed_input_tokens as i64;
usage.cache_creation_tokens = cache_creation_tokens as i64;
usage.cache_read_tokens = cache_read_tokens as i64;
Some(usage)
}
fn context_has_inline_body(context: Option<&Map<String, Value>>, key: &str) -> bool {
matches!(context_value_ref(context, key), Some(value) if !value.is_null())
}
@@ -5543,6 +5593,64 @@ mod tests {
);
}
#[test]
fn sync_terminal_usage_applies_kiro_simulated_cache_context() {
let plan = ExecutionPlan {
request_id: "req-sync-kiro-cache-context-1".to_string(),
candidate_id: Some("cand-sync-kiro-cache-context-1".to_string()),
provider_name: Some("Kiro".to_string()),
provider_id: "provider-kiro-1".to_string(),
endpoint_id: "endpoint-kiro-1".to_string(),
key_id: "key-kiro-1".to_string(),
method: "POST".to_string(),
url: "https://kiro.example/generateAssistantResponse".to_string(),
headers: BTreeMap::new(),
content_type: Some("application/json".to_string()),
content_encoding: None,
body: RequestBody::from_json(json!({
"model": "claude-sonnet-4",
"messages": [{"role": "user", "content": "hello kiro"}],
})),
stream: false,
client_api_format: "claude:messages".to_string(),
provider_api_format: "claude:messages".to_string(),
model_name: Some("claude-sonnet-4".to_string()),
proxy: None,
transport_profile: None,
timeouts: None,
};
let payload = GatewaySyncReportRequest {
trace_id: "trace-sync-kiro-cache-context-1".to_string(),
report_kind: "claude_cli_sync_success".to_string(),
report_context: Some(json!({
"client_api_format": "claude:messages",
"provider_api_format": "claude:messages",
"provider_name": "Kiro",
"model": "claude-sonnet-4",
"input_tokens": 1800,
"kiro_simulated_cache_enabled": true,
"cache_creation_input_tokens": 1200,
"cache_read_input_tokens": 300,
})),
status_code: 200,
headers: BTreeMap::new(),
body_json: Some(json!({"id": "kiro-sync-response-1"})),
client_body_json: None,
body_base64: None,
telemetry: None,
};
let event =
build_sync_terminal_usage_event(&plan, payload.report_context.as_ref(), &payload)
.expect("usage event should build");
assert_eq!(event.event_type, UsageEventType::Completed);
assert_eq!(event.data.input_tokens, Some(300));
assert_eq!(event.data.cache_creation_input_tokens, Some(1200));
assert_eq!(event.data.cache_read_input_tokens, Some(300));
assert_eq!(event.data.total_tokens, Some(300));
}
#[test]
fn sync_terminal_usage_treats_null_error_field_as_success() {
let plan = ExecutionPlan {