#[cfg(test)]
mod tests;
use super::usage::{OpenAiUsage, OpenAiUsageContext, normalize_usage};
use bcode_model::{TokenUsage, UsageCaptureSpec, UsageDecoder};
use bcode_session_models::OriginalUsage;
#[derive(Debug, Clone, Copy)]
pub enum ResponsesUsageDialect {
OpenAi,
Bedrock,
ChatCompletions,
}
impl UsageDecoder for ResponsesUsageDialect {
fn capture_spec(&self) -> UsageCaptureSpec {
match self {
Self::OpenAi | Self::Bedrock => UsageCaptureSpec {
api_shape: "responses",
containers: &[&["response"], &[]],
default_source: "usage",
confirmed_fields: &["model", "service_tier", "prompt_cache_retention"],
complete_sources: &[
"response.completed",
"response.done",
"response.incomplete",
"response.failed",
],
},
Self::ChatCompletions => UsageCaptureSpec {
api_shape: "chat_completions",
containers: &[&[]],
default_source: "usage",
confirmed_fields: &["model", "service_tier", "prompt_cache_retention"],
complete_sources: &["usage"],
},
}
}
fn observe_json(
&self,
_previous: Option<&TokenUsage>,
usage_json: &str,
source: &str,
requested: &std::collections::BTreeMap<String, String>,
confirmed: &std::collections::BTreeMap<String, String>,
) -> Result<TokenUsage, String> {
self.decode(usage_json, source, requested, confirmed)
}
fn normalize(&self, original: &OriginalUsage) -> Result<TokenUsage, String> {
let report = original.reports.last().ok_or("missing usage report")?;
self.decode(
&report.usage_json,
&report.source,
&original.requested,
&report.confirmed,
)
}
}
impl ResponsesUsageDialect {
fn decode(
self,
usage_json: &str,
source: &str,
requested: &std::collections::BTreeMap<String, String>,
confirmed: &std::collections::BTreeMap<String, String>,
) -> Result<TokenUsage, String> {
if !self.capture_spec().complete_sources.contains(&source) && source != "usage" {
return Err("unsupported usage source".into());
}
let mut usage: OpenAiUsage =
serde_json::from_str(usage_json).map_err(|_| "invalid usage report")?;
if let Some(tier) = confirmed.get("service_tier") {
usage.service_tier = Some(tier.clone());
}
let model = confirmed
.get("model")
.or_else(|| requested.get("model"))
.cloned();
let retention = confirmed
.get("prompt_cache_retention")
.or_else(|| requested.get("prompt_cache_retention"));
let tier = confirmed
.get("service_tier")
.or_else(|| {
requested.get("service_tier").filter(|tier| {
matches!(self, Self::Bedrock) || matches!(tier.as_str(), "default" | "standard")
})
})
.cloned();
let mut normalized = normalize_usage(
usage,
OpenAiUsageContext {
model,
service_tier: tier,
prompt_cache_retention: retention.cloned(),
},
);
if matches!(self, Self::Bedrock) {
normalized.pricing_context.cache_ttl_seconds =
retention.and_then(|ttl| match ttl.as_str() {
"30m" => Some(1800),
"1h" => Some(3600),
_ => None,
});
if !normalized.has_valid_input_breakdown() {
normalized.pricing_context.request_input_tokens = None;
}
if normalized.details.is_empty() {
normalized.details = super::usage::text_usage_details(&normalized);
} else {
for detail in &mut normalized.details {
if matches!(
detail.bucket,
bcode_model::ModelPricingBucket::CacheReadInput
| bcode_model::ModelPricingBucket::CacheWriteInput
) {
detail.cache_ttl_seconds = normalized.pricing_context.cache_ttl_seconds;
}
}
}
}
Ok(normalized)
}
}