use super::{
ContentBlock, ModelMessage, ModelTurnRequest, session_events_to_model_messages_with_limit,
};
pub const LOCAL_CONTEXT_ESTIMATOR_VERSION: u16 = 1;
pub fn local_request_estimate(
request: &ModelTurnRequest,
) -> bcode_session_models::LocalContextEstimate {
let tokens = serde_json::to_string(&(
request.system_prompt.as_ref(),
&request.messages,
&request.tools,
&request.parameters,
request.structured_output.as_ref(),
&request.provider_context.request,
))
.map_or(u64::MAX, |serialized| {
estimated_tokens_from_chars(serialized.chars().count())
})
.saturating_add(image_tokens(&request.messages));
bcode_session_models::LocalContextEstimate {
tokens,
algorithm_version: LOCAL_CONTEXT_ESTIMATOR_VERSION,
}
}
pub fn projected_model_context_chars(
history: &[bcode_session_models::SessionEvent],
tool_output_context_chars: usize,
) -> usize {
session_events_to_model_messages_with_limit(history, tool_output_context_chars)
.iter()
.map(model_message_context_chars)
.sum()
}
pub fn estimated_tokens_from_chars(chars: usize) -> u64 {
bcode_session_models::estimated_normalized_text_tokens_from_chars(chars)
}
pub fn estimated_model_messages_tokens(messages: &[ModelMessage]) -> u64 {
estimated_semantic_messages_tokens(messages)
}
fn estimated_semantic_messages_tokens(messages: &[ModelMessage]) -> u64 {
let structural = serde_json::to_vec(messages).map_or(u64::MAX, |serialized| {
u64::try_from(serialized.len())
.unwrap_or(u64::MAX)
.saturating_add(3)
/ 4
});
structural.saturating_add(image_tokens(messages))
}
fn image_tokens(messages: &[ModelMessage]) -> u64 {
messages
.iter()
.flat_map(|message| &message.content)
.map(|block| match block {
ContentBlock::Image { image } => image_metadata_tokens(&image.metadata),
ContentBlock::ToolResult { result } => result
.content
.iter()
.map(|content| match content {
bcode_model::ToolResultContent::Image { image } => {
image_metadata_tokens(&image.metadata)
}
bcode_model::ToolResultContent::ImageRef { image } => {
image_metadata_tokens(&image.metadata)
}
bcode_model::ToolResultContent::Text { .. } => 0,
})
.sum(),
ContentBlock::Text { .. }
| ContentBlock::ToolCall { .. }
| ContentBlock::CachePoint { .. }
| ContentBlock::ProviderExtension { .. } => 0,
})
.sum()
}
fn image_metadata_tokens(metadata: &bcode_model::ImageMetadata) -> u64 {
metadata
.width
.zip(metadata.height)
.map_or(0, |(width, height)| {
u64::from(width)
.saturating_mul(u64::from(height))
.saturating_add(749)
/ 750
})
}
pub fn model_message_context_chars(message: &ModelMessage) -> usize {
message
.content
.iter()
.map(|block| match block {
ContentBlock::Text { text } => text.chars().count(),
ContentBlock::Image { image } => {
usize::try_from(image_metadata_tokens(&image.metadata).saturating_mul(4))
.unwrap_or(usize::MAX)
}
ContentBlock::CachePoint { .. } => 0,
ContentBlock::ToolCall { call } => {
call.name.chars().count() + call.arguments.to_string().chars().count()
}
ContentBlock::ToolResult { result } => result.output.chars().count().saturating_add(
usize::try_from(
result
.content
.iter()
.map(|content| match content {
bcode_model::ToolResultContent::Image { image } => {
image_metadata_tokens(&image.metadata)
}
bcode_model::ToolResultContent::ImageRef { image } => {
image_metadata_tokens(&image.metadata)
}
bcode_model::ToolResultContent::Text { text } => {
estimated_tokens_from_chars(text.chars().count())
}
})
.sum::<u64>()
.saturating_mul(4),
)
.unwrap_or(usize::MAX),
),
ContentBlock::ProviderExtension { value } => value.to_string().chars().count(),
})
.sum()
}