use crate::{Message, Provider};
use serde::{Deserialize, Serialize};
use sha2::{Digest, Sha256};
#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "snake_case")]
pub enum LoweredRequestEncoding {
AnthropicMessagesJson,
OpenAiResponsesJson,
OpenAiChatCompletionsJson,
GeminiGenerateContentJson,
}
#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "snake_case", deny_unknown_fields)]
pub struct LoweredRequestProvenance {
pub provider: Provider,
pub encoding: LoweredRequestEncoding,
pub body_sha256: [u8; 32],
}
impl LoweredRequestProvenance {
pub fn from_body(
provider: Provider,
encoding: LoweredRequestEncoding,
encoded_body: &[u8],
) -> Self {
Self {
provider,
encoding,
body_sha256: Sha256::digest(encoded_body).into(),
}
}
}
#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "snake_case")]
pub enum PresentedTokenConvention {
AnthropicDisjointInputComponents,
OpenAiInputIncludesCachedSubset,
GeminiPromptIncludesCachedSubset,
OpenAiCompatiblePromptIncludesCacheDetails,
HostDeclaredInclusiveInputTotal,
}
#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "snake_case")]
pub enum TokenAggregationProvenance {
SumDisjointProviderComponents,
ProviderInclusiveInputTotal,
}
#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "snake_case", deny_unknown_fields)]
pub struct ProviderTokenAccounting {
pub provider: Provider,
pub model: String,
pub presented_tokens: u64,
pub convention: PresentedTokenConvention,
pub aggregation: TokenAggregationProvenance,
}
impl ProviderTokenAccounting {
pub fn anthropic(
model: impl Into<String>,
uncached_input: u64,
cache_creation_input: u64,
cache_read_input: u64,
) -> Self {
Self {
provider: Provider::Anthropic,
model: model.into(),
presented_tokens: uncached_input
.saturating_add(cache_creation_input)
.saturating_add(cache_read_input),
convention: PresentedTokenConvention::AnthropicDisjointInputComponents,
aggregation: TokenAggregationProvenance::SumDisjointProviderComponents,
}
}
pub fn openai(model: impl Into<String>, input_tokens: u64) -> Self {
Self {
provider: Provider::OpenAI,
model: model.into(),
presented_tokens: input_tokens,
convention: PresentedTokenConvention::OpenAiInputIncludesCachedSubset,
aggregation: TokenAggregationProvenance::ProviderInclusiveInputTotal,
}
}
pub fn gemini(model: impl Into<String>, prompt_tokens: u64) -> Self {
Self {
provider: Provider::Gemini,
model: model.into(),
presented_tokens: prompt_tokens,
convention: PresentedTokenConvention::GeminiPromptIncludesCachedSubset,
aggregation: TokenAggregationProvenance::ProviderInclusiveInputTotal,
}
}
pub fn openai_compatible(model: impl Into<String>, prompt_tokens: u64) -> Self {
Self {
provider: Provider::SelfHosted,
model: model.into(),
presented_tokens: prompt_tokens,
convention: PresentedTokenConvention::OpenAiCompatiblePromptIncludesCacheDetails,
aggregation: TokenAggregationProvenance::ProviderInclusiveInputTotal,
}
}
pub fn host_declared(provider: Provider, model: impl Into<String>, input_tokens: u64) -> Self {
Self {
provider,
model: model.into(),
presented_tokens: input_tokens,
convention: PresentedTokenConvention::HostDeclaredInclusiveInputTotal,
aggregation: TokenAggregationProvenance::ProviderInclusiveInputTotal,
}
}
}
#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
#[serde(tag = "kind", rename_all = "snake_case")]
pub enum CacheBreakpointBoundary {
SystemProfilePrefix { message_count: u64 },
TranscriptAfter { message_count: u64 },
}
impl CacheBreakpointBoundary {
pub const fn message_count(self) -> u64 {
match self {
Self::SystemProfilePrefix { message_count }
| Self::TranscriptAfter { message_count } => message_count,
}
}
}
#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "snake_case")]
pub enum ProviderCacheTtl {
FiveMinutes,
OneHour,
ThirtyMinutes,
TwentyFourHours,
ProviderDefault,
}
#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "snake_case", deny_unknown_fields)]
pub struct AuthoredCacheBreakpoint {
provider: Provider,
model: String,
boundary: CacheBreakpointBoundary,
canonical_prefix_sha256: String,
canonical_prefix_bytes: u64,
rendered_prefix_sha256: String,
rendered_prefix_bytes: u64,
lowered_request_provenance: LoweredRequestProvenance,
ttl: ProviderCacheTtl,
}
#[derive(Debug, Clone, PartialEq, Eq, thiserror::Error)]
pub enum CacheBreakpointEvidenceError {
#[error("cache breakpoint boundary {message_count} exceeds transcript length {message_len}")]
BoundaryOutOfRange {
message_count: u64,
message_len: usize,
},
#[error("cache breakpoint prefix could not be canonically encoded: {detail}")]
CanonicalEncodingFailed { detail: String },
#[error("persisted cache-breakpoint evidence is malformed: {detail}")]
PersistedEvidenceMalformed { detail: String },
#[error("cache-breakpoint evidence does not match the canonical transcript prefix")]
CanonicalPrefixMismatch,
#[error("cache-breakpoint rendered-prefix evidence is malformed")]
RenderedPrefixMalformed,
#[error("cache-breakpoint provider and lowered-request encoding are incoherent")]
ProviderEncodingMismatch,
}
impl AuthoredCacheBreakpoint {
pub(crate) fn from_provider_claim(claim: ProviderCacheBreakpointClaim) -> Self {
claim.evidence
}
pub const fn provider(&self) -> Provider {
self.provider
}
pub fn model(&self) -> &str {
&self.model
}
pub const fn boundary(&self) -> CacheBreakpointBoundary {
self.boundary
}
pub fn canonical_prefix_sha256(&self) -> &str {
&self.canonical_prefix_sha256
}
pub const fn canonical_prefix_bytes(&self) -> u64 {
self.canonical_prefix_bytes
}
pub fn rendered_prefix_sha256(&self) -> &str {
&self.rendered_prefix_sha256
}
pub const fn rendered_prefix_bytes(&self) -> u64 {
self.rendered_prefix_bytes
}
pub const fn lowered_request_provenance(&self) -> LoweredRequestProvenance {
self.lowered_request_provenance
}
pub const fn ttl(&self) -> ProviderCacheTtl {
self.ttl
}
pub fn validate_rendered_identity(&self) -> Result<(), CacheBreakpointEvidenceError> {
let hash = self.rendered_prefix_sha256.as_bytes();
let valid_hash = hash.len() == 71
&& hash.starts_with(b"sha256:")
&& hash[7..]
.iter()
.all(|byte| byte.is_ascii_digit() || (b'a'..=b'f').contains(byte));
if !valid_hash || self.rendered_prefix_bytes == 0 {
return Err(CacheBreakpointEvidenceError::RenderedPrefixMalformed);
}
let coherent_encoding = matches!(
(self.provider, self.lowered_request_provenance.encoding),
(
Provider::Anthropic,
LoweredRequestEncoding::AnthropicMessagesJson
) | (
Provider::Gemini,
LoweredRequestEncoding::GeminiGenerateContentJson
) | (
Provider::OpenAI | Provider::SelfHosted,
LoweredRequestEncoding::OpenAiResponsesJson
| LoweredRequestEncoding::OpenAiChatCompletionsJson
)
) && self.lowered_request_provenance.provider == self.provider;
if !coherent_encoding {
return Err(CacheBreakpointEvidenceError::ProviderEncodingMismatch);
}
Ok(())
}
}
#[derive(Debug, Clone)]
pub struct ProviderCacheBreakpointClaim {
evidence: AuthoredCacheBreakpoint,
}
impl ProviderCacheBreakpointClaim {
pub const fn provider(&self) -> Provider {
self.evidence.provider()
}
pub fn model(&self) -> &str {
self.evidence.model()
}
}
#[derive(Debug)]
pub struct ValidatedSourceCacheBreakpoint {
evidence: AuthoredCacheBreakpoint,
}
impl ValidatedSourceCacheBreakpoint {
pub(crate) fn new(evidence: AuthoredCacheBreakpoint) -> Self {
Self { evidence }
}
pub const fn provider(&self) -> Provider {
self.evidence.provider()
}
pub fn model(&self) -> &str {
self.evidence.model()
}
pub const fn boundary(&self) -> CacheBreakpointBoundary {
self.evidence.boundary()
}
pub(crate) fn into_authored_evidence(self) -> AuthoredCacheBreakpoint {
self.evidence
}
}
#[derive(Debug)]
pub struct TargetCacheLoweringCapability {
evidence: AuthoredCacheBreakpoint,
}
impl TargetCacheLoweringCapability {
pub const fn provider(&self) -> Provider {
self.evidence.provider()
}
pub fn model(&self) -> &str {
self.evidence.model()
}
pub const fn boundary(&self) -> CacheBreakpointBoundary {
self.evidence.boundary()
}
pub fn rendered_prefix_sha256(&self) -> &str {
self.evidence.rendered_prefix_sha256()
}
pub const fn rendered_prefix_bytes(&self) -> u64 {
self.evidence.rendered_prefix_bytes()
}
pub const fn lowered_request_provenance(&self) -> LoweredRequestProvenance {
self.evidence.lowered_request_provenance()
}
pub const fn ttl(&self) -> ProviderCacheTtl {
self.evidence.ttl()
}
pub(crate) fn into_authored_evidence(self) -> AuthoredCacheBreakpoint {
self.evidence
}
}
#[derive(Debug)]
pub struct TargetCacheLoweringIssuer {
_private: (),
}
impl TargetCacheLoweringIssuer {
pub(crate) const fn new() -> Self {
Self { _private: () }
}
pub fn mint(
&self,
claim: ProviderCacheBreakpointClaim,
) -> Result<TargetCacheLoweringCapability, CacheBreakpointEvidenceError> {
let evidence = AuthoredCacheBreakpoint::from_provider_claim(claim);
evidence.validate_rendered_identity()?;
Ok(TargetCacheLoweringCapability { evidence })
}
}
pub fn canonical_cache_prefix_identity(
messages: &[Message],
message_count: u64,
) -> Result<(String, u64), CacheBreakpointEvidenceError> {
let boundary = usize::try_from(message_count).map_err(|_| {
CacheBreakpointEvidenceError::BoundaryOutOfRange {
message_count,
message_len: messages.len(),
}
})?;
let prefix =
messages
.get(..boundary)
.ok_or(CacheBreakpointEvidenceError::BoundaryOutOfRange {
message_count,
message_len: messages.len(),
})?;
crate::session::canonical_transcript_prefix_identity(prefix).map_err(|error| {
CacheBreakpointEvidenceError::CanonicalEncodingFailed {
detail: error.to_string(),
}
})
}
pub struct ProviderCacheBreakpointClaimRequest<'a> {
pub provider: Provider,
pub model: &'a str,
pub messages: &'a [Message],
pub boundary: CacheBreakpointBoundary,
pub ttl: ProviderCacheTtl,
pub rendered_prefix: &'a [u8],
pub lowered_request_encoding: LoweredRequestEncoding,
pub lowered_request_body: &'a [u8],
}
pub fn provider_cache_breakpoint_claim(
request: ProviderCacheBreakpointClaimRequest<'_>,
) -> Result<ProviderCacheBreakpointClaim, CacheBreakpointEvidenceError> {
let (canonical_prefix_sha256, canonical_prefix_bytes) =
canonical_cache_prefix_identity(request.messages, request.boundary.message_count())?;
let rendered_prefix_sha256 = format!("sha256:{:x}", Sha256::digest(request.rendered_prefix));
let rendered_prefix_bytes = u64::try_from(request.rendered_prefix.len()).unwrap_or(u64::MAX);
let lowered_request_provenance = LoweredRequestProvenance::from_body(
request.provider,
request.lowered_request_encoding,
request.lowered_request_body,
);
let evidence = AuthoredCacheBreakpoint {
provider: request.provider,
model: request.model.to_string(),
boundary: request.boundary,
canonical_prefix_sha256,
canonical_prefix_bytes,
rendered_prefix_sha256,
rendered_prefix_bytes,
lowered_request_provenance,
ttl: request.ttl,
};
evidence.validate_rendered_identity()?;
Ok(ProviderCacheBreakpointClaim { evidence })
}