use std::collections::BTreeMap;
#[derive(Clone, Debug, PartialEq)]
#[non_exhaustive]
pub struct IrRequest {
pub model: String,
pub items: Vec<IrItem>,
pub tools: Vec<IrTool>,
pub sampling: IrSampling,
}
impl IrRequest {
#[must_use]
pub fn new(model: impl AsRef<str>, items: Vec<IrItem>) -> Self {
Self {
model: model.as_ref().to_owned(),
items,
tools: Vec::new(),
sampling: IrSampling::default(),
}
}
#[must_use]
pub fn with_tools(mut self, tools: Vec<IrTool>) -> Self {
self.tools = tools;
self
}
#[must_use]
pub fn with_sampling(mut self, sampling: IrSampling) -> Self {
self.sampling = sampling;
self
}
}
impl Default for IrRequest {
fn default() -> Self {
Self::new(String::new(), Vec::new())
}
}
#[derive(Clone, Debug, Default, PartialEq)]
#[non_exhaustive]
pub struct IrSampling {
pub temperature: Option<f32>,
pub top_p: Option<f32>,
pub top_k: Option<u32>,
pub max_tokens: Option<u32>,
pub stop: Vec<String>,
pub tool_choice: IrToolChoice,
pub parallel_tool_calls: Option<bool>,
pub store: Option<bool>,
pub previous_response_id: Option<String>,
pub cache: IrCache,
pub stream: Option<bool>,
pub include_thoughts: Option<bool>,
pub thinking_budget: Option<u32>,
pub reasoning_effort: Option<String>,
pub max_reasoning_tokens: Option<u32>,
pub json_schema: Option<serde_json::Value>,
pub json_schema_name: Option<String>,
pub json_schema_description: Option<String>,
pub json_schema_strict: Option<bool>,
pub json_object: Option<bool>,
pub include: Vec<String>,
pub prompt_cache_key: Option<String>,
pub prompt_cache_retention: Option<String>,
pub prompt_cache_mode: Option<String>,
pub prompt_cache_ttl: Option<String>,
pub top_logprobs: Option<u32>,
pub moderation_model: Option<String>,
pub moderation_input: Option<String>,
pub moderation_output: Option<String>,
pub include_obfuscation: Option<bool>,
pub user: Option<String>,
pub verbosity: Option<String>,
pub safety_identifier: Option<String>,
pub metadata: BTreeMap<String, String>,
pub frequency_penalty: Option<f32>,
pub presence_penalty: Option<f32>,
pub seed: Option<i64>,
pub n: Option<u32>,
pub service_tier: Option<String>,
pub output_modalities: Vec<String>,
pub audio_voice: Option<String>,
pub audio_format: Option<String>,
pub logprobs: Option<bool>,
pub logit_bias: BTreeMap<String, f64>,
pub prediction: Option<serde_json::Value>,
pub web_search_options: Option<serde_json::Value>,
pub inference_geo: Option<String>,
pub performance_latency: Option<String>,
pub previous_message_id: Option<String>,
pub response_field_paths: Vec<String>,
pub additional_request_fields: Option<serde_json::Value>,
pub prompt_variables: Option<serde_json::Value>,
pub guardrail: Option<serde_json::Value>,
pub container: Option<serde_json::Value>,
pub context_management: Option<serde_json::Value>,
pub mcp_servers: Option<serde_json::Value>,
pub media_resolution: Option<String>,
pub image_config: Option<serde_json::Value>,
pub audio_transcription_config: Option<serde_json::Value>,
pub translation_config: Option<serde_json::Value>,
pub affective_dialog: Option<bool>,
pub enhanced_civic_answers: Option<bool>,
pub multi_speaker_speech: Option<serde_json::Value>,
pub cached_content: Option<String>,
pub safety_settings: Option<serde_json::Value>,
pub request_labels: Option<serde_json::Value>,
pub truncation: Option<String>,
pub max_tool_calls: Option<u32>,
pub background: Option<bool>,
pub conversation: Option<serde_json::Value>,
pub responses_context_management: Option<serde_json::Value>,
pub messages_thinking: Option<serde_json::Value>,
}
impl IrSampling {
pub fn patch(update: impl FnOnce(&mut Self)) -> Self {
let mut sampling = Self::default();
update(&mut sampling);
sampling
}
}
#[derive(Clone, Debug, Default, Eq, PartialEq)]
#[non_exhaustive]
pub enum IrToolChoice {
#[default]
Auto,
None,
Required,
Named(String),
}
#[derive(Clone, Debug, Default, Eq, PartialEq)]
#[non_exhaustive]
pub struct IrCache {
pub enabled: bool,
pub retention: Option<String>,
pub min_cacheable_tokens: Option<u32>,
}
impl IrCache {
#[must_use]
pub fn enabled() -> Self {
Self {
enabled: true,
..Self::default()
}
}
#[must_use]
pub fn with_retention(mut self, retention: impl AsRef<str>) -> Self {
self.retention = Some(retention.as_ref().to_owned());
self
}
#[must_use]
pub fn with_min_cacheable_tokens(mut self, tokens: u32) -> Self {
self.min_cacheable_tokens = Some(tokens);
self
}
}
#[derive(Clone, Debug, PartialEq)]
#[non_exhaustive]
pub enum IrItem {
System {
text: String,
prompt_cache_breakpoint: Option<String>,
content_parts: Vec<IrPart>,
},
SystemCache {
raw: serde_json::Value,
},
SystemGuard {
raw: serde_json::Value,
},
Developer {
text: String,
prompt_cache_breakpoint: Option<String>,
content_parts: Vec<IrPart>,
},
User {
parts: Vec<IrPart>,
},
Assistant {
parts: Vec<IrPart>,
phase: Option<String>,
},
FunctionCall {
call_id: String,
name: String,
arguments: String,
thought_signature: Option<String>,
},
CustomToolCall {
call_id: String,
name: String,
input: String,
responses_item: Option<serde_json::Value>,
},
FunctionOutput {
call_id: String,
output: String,
parts: Vec<IrPart>,
is_error: bool,
},
Reasoning {
encrypted: Option<String>,
summary: Option<String>,
raw: Option<serde_json::Value>,
},
HostedToolCall {
kind: String,
raw: serde_json::Value,
},
Unknown {
type_name: String,
raw: serde_json::Value,
},
}
#[derive(Clone, Debug, PartialEq)]
#[non_exhaustive]
pub enum IrPart {
Text(String),
ImageUrl {
url: String,
detail: Option<String>,
bucket_owner: Option<String>,
file_id: Option<String>,
},
ImageBase64 {
media_type: String,
data: String,
},
Thinking {
text: String,
signature: Option<String>,
},
Raw {
type_name: String,
raw: serde_json::Value,
},
Document {
source: IrDocumentSource,
media_type: String,
name: Option<String>,
citations_enabled: Option<bool>,
context: Option<String>,
},
Audio {
data: String,
format: String,
},
PromptCacheBreakpoint {
mode: String,
inner: Box<IrPart>,
},
}
#[derive(Clone, Debug, PartialEq)]
#[non_exhaustive]
pub enum IrDocumentSource {
Base64(String),
Text(String),
Url(String),
S3 {
uri: String,
bucket_owner: Option<String>,
},
FileId(String),
}
#[derive(Clone, Debug, PartialEq)]
#[non_exhaustive]
pub enum IrTool {
Function {
name: String,
description: String,
parameters: serde_json::Value,
strict: Option<bool>,
},
Namespace {
name: String,
raw: serde_json::Value,
},
Hosted {
kind: String,
raw: serde_json::Value,
},
Unknown {
type_name: String,
raw: serde_json::Value,
},
CachePoint { raw: serde_json::Value },
}
#[derive(Clone, Debug, PartialEq)]
#[non_exhaustive]
pub enum IrStreamEvent {
TextDelta {
text: String,
},
RefusalDelta {
text: String,
},
ReasoningDelta {
text: String,
},
ReasoningSignature {
signature: String,
},
ToolCallStart {
id: String,
name: String,
thought_signature: Option<String>,
index: u32,
},
ToolCallArgDelta {
delta: String,
index: u32,
},
ToolCallEnd,
AnnotationAdded {
annotation: serde_json::Value,
},
SearchEntryPoint {
rendered_content: String,
},
AudioDelta {
data: String,
},
ImageDelta {
media_type: String,
data: String,
},
AudioTranscriptDelta {
text: String,
},
CustomToolCallStart {
id: String,
name: String,
index: u32,
},
CustomToolCallInputDelta {
delta: String,
index: u32,
},
Logprobs {
content: serde_json::Value,
refusal: Option<serde_json::Value>,
},
Created {
unix: i64,
},
ServiceTier {
tier: String,
},
Metadata {
metadata: BTreeMap<String, String>,
},
Moderation {
input: Option<serde_json::Value>,
output: Option<serde_json::Value>,
},
Diagnostics {
cache_miss_reason: serde_json::Value,
},
Container {
value: serde_json::Value,
},
ContextManagement {
value: serde_json::Value,
},
StopSequence {
text: String,
},
Usage {
prompt_tokens: u32,
completion_tokens: u32,
cache_read_tokens: u32,
cache_write_tokens: u32,
reasoning_tokens: u32,
audio_tokens: u32,
completion_audio_tokens: u32,
inference_geo: Option<String>,
},
FinishReason {
reason: String,
vendor: Option<String>,
},
Protocol {
item_type: String,
payload: serde_json::Value,
},
Unknown {
event: String,
raw: serde_json::Value,
},
Done,
}
#[derive(Clone, Debug, Default, Eq, PartialEq)]
#[non_exhaustive]
pub struct LossReport {
pub events: Vec<LossEvent>,
}
impl LossReport {
pub fn record(
&mut self,
path: impl Into<String>,
action: LossAction,
detail: impl Into<String>,
) {
self.events.push(LossEvent {
path: path.into(),
action,
detail: detail.into(),
});
}
#[must_use]
pub fn lossy(&self) -> Vec<LossEvent> {
self.events
.iter()
.filter(|event| event.action != LossAction::Preserve)
.cloned()
.collect()
}
}
#[derive(Clone, Debug, Eq, PartialEq)]
#[non_exhaustive]
pub struct LossEvent {
pub path: String,
pub action: LossAction,
pub detail: String,
}
impl std::fmt::Display for LossEvent {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
let action = match self.action {
LossAction::Preserve => "preserve",
LossAction::Degrade => "degrade",
LossAction::Drop => "drop",
LossAction::HardError => "harderror",
};
write!(f, "{action} {}: {}", self.path, self.detail)
}
}
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
#[non_exhaustive]
pub enum LossAction {
Preserve,
Degrade,
Drop,
HardError,
}
#[must_use]
pub fn estimate_prompt_tokens(req: &IrRequest) -> u32 {
crate::map::messages_cache_floor_tokens(req)
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn ir_request_holds_system_user_and_function_tool() {
let req = IrRequest {
model: "gpt-4".into(),
items: vec![
IrItem::System {
text: "You are helpful.".into(),
prompt_cache_breakpoint: None,
content_parts: vec![],
},
IrItem::User {
parts: vec![IrPart::Text("Hi".into())],
},
],
tools: vec![IrTool::Function {
name: "lookup".into(),
description: "Look up a term.".into(),
parameters: serde_json::json!({"type": "object"}),
strict: None,
}],
sampling: IrSampling::default(),
};
assert_eq!(req.model, "gpt-4");
assert_eq!(req.items.len(), 2);
assert!(matches!(req.items[0], IrItem::System { .. }));
assert!(matches!(
&req.items[1],
IrItem::User { parts } if parts == &[IrPart::Text("Hi".into())]
));
assert_eq!(req.tools.len(), 1);
assert!(matches!(
&req.tools[0],
IrTool::Function { name, description, .. }
if name == "lookup" && description == "Look up a term."
));
assert!(matches!(req.sampling.tool_choice, IrToolChoice::Auto));
}
#[test]
fn loss_report_records_all_actions() {
let mut report = LossReport::default();
report.record("items[0]", LossAction::Preserve, "kept as-is");
report.record("items[1]", LossAction::Degrade, "developer to system");
report.record("sampling.store", LossAction::Drop, "no slot");
report.record("tools[0]", LossAction::HardError, "namespace");
assert_eq!(
report.events,
[
LossEvent {
path: "items[0]".into(),
action: LossAction::Preserve,
detail: "kept as-is".into(),
},
LossEvent {
path: "items[1]".into(),
action: LossAction::Degrade,
detail: "developer to system".into(),
},
LossEvent {
path: "sampling.store".into(),
action: LossAction::Drop,
detail: "no slot".into(),
},
LossEvent {
path: "tools[0]".into(),
action: LossAction::HardError,
detail: "namespace".into(),
},
]
);
}
#[test]
fn ir_tool_namespace_is_first_class() {
let tool = IrTool::Namespace {
name: "apply_patch".into(),
raw: serde_json::json!({"type": "namespace"}),
};
assert!(matches!(
&tool,
IrTool::Namespace { name, .. } if name == "apply_patch"
));
assert!(!matches!(tool, IrTool::Function { .. }));
}
#[test]
fn ir_item_developer_is_distinct_from_system() {
let system = IrItem::System {
text: "rules".into(),
prompt_cache_breakpoint: None,
content_parts: vec![],
};
let developer = IrItem::Developer {
text: "rules".into(),
prompt_cache_breakpoint: None,
content_parts: vec![],
};
assert_ne!(system, developer);
assert!(matches!(system, IrItem::System { .. }));
assert!(matches!(developer, IrItem::Developer { .. }));
}
#[test]
fn estimate_prompt_tokens_counts_developer_text() {
let req = IrRequest {
model: "claude".into(),
items: vec![IrItem::Developer {
text: "x".repeat(5000),
prompt_cache_breakpoint: None,
content_parts: vec![],
}],
tools: vec![],
sampling: IrSampling::default(),
};
assert!(
estimate_prompt_tokens(&req) >= 1024,
"5000 developer chars must meet a 1024-token floor"
);
}
#[test]
fn estimate_prompt_tokens_counts_function_output() {
let req = IrRequest {
model: "claude".into(),
items: vec![IrItem::FunctionOutput {
call_id: "c1".into(),
output: "x".repeat(5000),
parts: Vec::new(),
is_error: false,
}],
tools: vec![],
sampling: IrSampling::default(),
};
assert!(
estimate_prompt_tokens(&req) >= 1024,
"5000 tool-result chars must meet a 1024-token floor"
);
}
#[test]
fn estimate_prompt_tokens_counts_signed_thinking_not_unsigned() {
let signed = IrRequest {
model: "claude".into(),
items: vec![IrItem::Assistant {
phase: None,
parts: vec![IrPart::Thinking {
text: "x".repeat(5000),
signature: Some("sig".into()),
}],
}],
tools: vec![],
sampling: IrSampling::default(),
};
assert!(
estimate_prompt_tokens(&signed) >= 1024,
"5000 signed thinking chars must meet a 1024-token floor"
);
let unsigned = IrRequest {
model: "claude".into(),
items: vec![IrItem::User {
parts: vec![
IrPart::Text("hi".into()),
IrPart::Thinking {
text: "x".repeat(5000),
signature: None,
},
],
}],
tools: vec![],
sampling: IrSampling::default(),
};
assert!(
estimate_prompt_tokens(&unsigned) < 1024,
"unsigned thinking is not replayed and must stay under the floor"
);
let blank_signature = IrRequest {
model: "claude".into(),
items: vec![IrItem::Assistant {
phase: None,
parts: vec![IrPart::Thinking {
text: "x".repeat(5000),
signature: Some(String::new()),
}],
}],
tools: vec![],
sampling: IrSampling::default(),
};
assert!(
estimate_prompt_tokens(&blank_signature) < 1024,
"an empty thinking signature is unsigned and must stay under the floor"
);
let signature_only = IrRequest {
model: "claude".into(),
items: vec![IrItem::Assistant {
phase: None,
parts: vec![IrPart::Thinking {
text: "hi".into(),
signature: Some("y".repeat(5000)),
}],
}],
tools: vec![],
sampling: IrSampling::default(),
};
assert!(
estimate_prompt_tokens(&signature_only) < 1024,
"a thinking signature is not prompt text"
);
}
#[test]
fn estimate_prompt_tokens_counts_images_documents_and_tool_result_parts() {
let long = "x".repeat(5000);
let image = IrRequest {
model: "claude".into(),
items: vec![IrItem::User {
parts: vec![IrPart::ImageBase64 {
media_type: "image/png".into(),
data: long.clone(),
}],
}],
tools: vec![],
sampling: IrSampling::default(),
};
assert!(
estimate_prompt_tokens(&image) >= 1024,
"5000 image bytes must meet a 1024-token floor"
);
let image_url = IrRequest {
model: "claude".into(),
items: vec![IrItem::User {
parts: vec![IrPart::ImageUrl {
url: long.clone(),
detail: Some("y".repeat(5000)),
bucket_owner: None,
file_id: None,
}],
}],
tools: vec![],
sampling: IrSampling::default(),
};
assert!(
estimate_prompt_tokens(&image_url) >= 1024,
"a long image URL must meet a 1024-token floor"
);
let detail_only = IrRequest {
model: "claude".into(),
items: vec![IrItem::User {
parts: vec![IrPart::ImageUrl {
url: "https://cdn.example/a.png".into(),
detail: Some(long.clone()),
bucket_owner: None,
file_id: None,
}],
}],
tools: vec![],
sampling: IrSampling::default(),
};
assert!(
estimate_prompt_tokens(&detail_only) >= 1024,
"a normal image URL meets a 1024-token floor"
);
let url_only = IrRequest {
model: "claude".into(),
items: vec![IrItem::User {
parts: vec![IrPart::ImageUrl {
url: "https://cdn.example/a.png".into(),
detail: None,
bucket_owner: None,
file_id: None,
}],
}],
tools: vec![],
sampling: IrSampling::default(),
};
assert_eq!(
estimate_prompt_tokens(&detail_only),
estimate_prompt_tokens(&url_only),
"image detail is dropped"
);
let file_id = IrRequest {
model: "claude".into(),
items: vec![IrItem::User {
parts: vec![IrPart::Document {
source: IrDocumentSource::FileId("file_abc".into()),
media_type: "application/pdf".into(),
name: None,
citations_enabled: None,
context: None,
}],
}],
tools: vec![],
sampling: IrSampling::default(),
};
assert!(
estimate_prompt_tokens(&file_id) >= 1024,
"a file id meets a 1024-token floor"
);
let short_base64 = IrRequest {
model: "claude".into(),
items: vec![IrItem::User {
parts: vec![IrPart::Document {
source: IrDocumentSource::Base64("0123456789".into()),
media_type: "application/pdf".into(),
name: None,
citations_enabled: None,
context: None,
}],
}],
tools: vec![],
sampling: IrSampling::default(),
};
assert!(
estimate_prompt_tokens(&short_base64) < 1024,
"short document base64 stays under the floor"
);
let document_text = IrRequest {
model: "claude".into(),
items: vec![IrItem::User {
parts: vec![IrPart::Document {
source: IrDocumentSource::Text(long.clone()),
media_type: "text/plain".into(),
name: None,
citations_enabled: None,
context: None,
}],
}],
tools: vec![],
sampling: IrSampling::default(),
};
assert!(
estimate_prompt_tokens(&document_text) >= 1024,
"5000 document text chars must meet a 1024-token floor"
);
let document_base64 = IrRequest {
model: "claude".into(),
items: vec![IrItem::User {
parts: vec![IrPart::Document {
source: IrDocumentSource::Base64(long.clone()),
media_type: "application/pdf".into(),
name: None,
citations_enabled: None,
context: None,
}],
}],
tools: vec![],
sampling: IrSampling::default(),
};
assert!(
estimate_prompt_tokens(&document_base64) >= 1024,
"5000 document base64 chars must meet a 1024-token floor"
);
let document_url = IrRequest {
model: "claude".into(),
items: vec![IrItem::User {
parts: vec![IrPart::Document {
source: IrDocumentSource::Url(long.clone()),
media_type: "application/pdf".into(),
name: None,
citations_enabled: None,
context: None,
}],
}],
tools: vec![],
sampling: IrSampling::default(),
};
assert!(
estimate_prompt_tokens(&document_url) >= 1024,
"a long document URL must meet a 1024-token floor"
);
let document_context = IrRequest {
model: "claude".into(),
items: vec![IrItem::User {
parts: vec![IrPart::Document {
source: IrDocumentSource::FileId("file-short".into()),
media_type: "text/plain".into(),
name: None,
citations_enabled: None,
context: Some(long.clone()),
}],
}],
tools: vec![],
sampling: IrSampling::default(),
};
assert!(
estimate_prompt_tokens(&document_context) >= 1024,
"document context Messages writes must meet a 1024-token floor"
);
let tool_image = IrRequest {
model: "claude".into(),
items: vec![IrItem::FunctionOutput {
call_id: "c1".into(),
output: String::new(),
parts: vec![IrPart::ImageBase64 {
media_type: "image/png".into(),
data: long.clone(),
}],
is_error: false,
}],
tools: vec![],
sampling: IrSampling::default(),
};
assert!(
estimate_prompt_tokens(&tool_image) >= 1024,
"a tool-result image must meet a 1024-token floor"
);
let tool_text = IrRequest {
model: "claude".into(),
items: vec![IrItem::FunctionOutput {
call_id: "c1".into(),
output: String::new(),
parts: vec![IrPart::Text(long.clone())],
is_error: false,
}],
tools: vec![],
sampling: IrSampling::default(),
};
assert!(
estimate_prompt_tokens(&tool_text) >= 1024,
"tool-result text Messages writes must meet a 1024-token floor"
);
let raw_image = IrRequest {
model: "claude".into(),
items: vec![IrItem::User {
parts: vec![IrPart::Raw {
type_name: "inline".into(),
raw: serde_json::json!({
"inlineData": {
"mimeType": "image/png",
"data": long,
}
}),
}],
}],
tools: vec![],
sampling: IrSampling::default(),
};
assert!(
estimate_prompt_tokens(&raw_image) >= 1024,
"inline image bytes Messages writes must meet a 1024-token floor"
);
let hinted_pdf = IrRequest {
model: "claude".into(),
items: vec![IrItem::User {
parts: vec![IrPart::Raw {
type_name: "inline".into(),
raw: serde_json::json!({
"inlineData": {
"mimeType": "application/pdf",
"data": "y".repeat(5000),
},
"mediaResolution": { "level": "media_resolution_high" }
}),
}],
}],
tools: vec![],
sampling: IrSampling::default(),
};
assert!(
estimate_prompt_tokens(&hinted_pdf) >= 1024,
"a media-hint PDF Messages writes must meet a 1024-token floor"
);
}
#[test]
fn estimate_prompt_tokens_skips_text_messages_does_not_send() {
let long = "x".repeat(5000);
let whitespace = IrRequest {
model: "claude".into(),
items: vec![IrItem::User {
parts: vec![IrPart::Text(" \n".repeat(2500))],
}],
tools: vec![],
sampling: IrSampling::default(),
};
assert!(
estimate_prompt_tokens(&whitespace) < 1024,
"whitespace-only text is omitted and must stay under the floor"
);
let audio = IrRequest {
model: "claude".into(),
items: vec![IrItem::User {
parts: vec![IrPart::Audio {
data: long.clone(),
format: "mp3".into(),
}],
}],
tools: vec![],
sampling: IrSampling::default(),
};
assert!(
estimate_prompt_tokens(&audio) < 1024,
"audio has no Messages slot and must stay under the floor"
);
let raw_json = IrRequest {
model: "claude".into(),
items: vec![IrItem::Unknown {
type_name: "redacted_thinking".into(),
raw: serde_json::json!({
"type": "redacted_thinking",
"data": long.clone(),
}),
}],
tools: vec![],
sampling: IrSampling::default(),
};
assert!(
estimate_prompt_tokens(&raw_json) < 1024,
"unknown raw JSON must not meet the floor"
);
let hosted = IrRequest {
model: "claude".into(),
items: vec![IrItem::HostedToolCall {
kind: "web_search".into(),
raw: serde_json::json!({ "query": long }),
}],
tools: vec![],
sampling: IrSampling::default(),
};
assert!(
estimate_prompt_tokens(&hosted) < 1024,
"hosted raw JSON must not meet the floor"
);
let raw_part = IrRequest {
model: "claude".into(),
items: vec![IrItem::User {
parts: vec![IrPart::Raw {
type_name: "redacted_thinking".into(),
raw: serde_json::json!({
"type": "redacted_thinking",
"data": long.clone(),
}),
}],
}],
tools: vec![],
sampling: IrSampling::default(),
};
assert!(
estimate_prompt_tokens(&raw_part) < 1024,
"a raw Messages block is not prompt text"
);
let raw_audio = IrRequest {
model: "claude".into(),
items: vec![IrItem::User {
parts: vec![IrPart::Raw {
type_name: "inline".into(),
raw: serde_json::json!({
"inlineData": {
"mimeType": "audio/mp3",
"data": long,
},
"mediaResolution": { "level": "media_resolution_high" }
}),
}],
}],
tools: vec![],
sampling: IrSampling::default(),
};
assert!(
estimate_prompt_tokens(&raw_audio) < 1024,
"audio bytes inside a raw part must stay under the floor"
);
let hinted_blank = IrRequest {
model: "claude".into(),
items: vec![IrItem::User {
parts: vec![IrPart::Raw {
type_name: "inline".into(),
raw: serde_json::json!({
"text": " \n".repeat(2500),
"mediaResolution": { "level": "media_resolution_high" }
}),
}],
}],
tools: vec![],
sampling: IrSampling::default(),
};
assert!(
estimate_prompt_tokens(&hinted_blank) < 1024,
"whitespace inside a hinted raw part must stay under the floor"
);
}
#[test]
fn estimate_prompt_tokens_counts_custom_tool_input_not_responses_item() {
let long = "x".repeat(5000);
let counted = IrRequest {
model: "claude".into(),
items: vec![IrItem::CustomToolCall {
call_id: "c1".into(),
name: "lookup".into(),
input: long.clone(),
responses_item: None,
}],
tools: vec![],
sampling: IrSampling::default(),
};
assert!(
estimate_prompt_tokens(&counted) >= 1024,
"5000 custom-tool input chars must meet a 1024-token floor"
);
let wrapper_only = IrRequest {
model: "claude".into(),
items: vec![IrItem::CustomToolCall {
call_id: "c1".into(),
name: "lookup".into(),
input: "{}".into(),
responses_item: Some(serde_json::json!({ "input": long })),
}],
tools: vec![],
sampling: IrSampling::default(),
};
assert!(
estimate_prompt_tokens(&wrapper_only) < 1024,
"responses_item must not double-count the custom-tool input"
);
}
#[test]
fn estimate_prompt_tokens_counts_reasoning_summary_not_signature() {
let long = "x".repeat(5000);
let summary = IrRequest {
model: "claude".into(),
items: vec![IrItem::Reasoning {
encrypted: Some("y".repeat(5000)),
summary: Some(long.clone()),
raw: None,
}],
tools: vec![],
sampling: IrSampling::default(),
};
assert!(
estimate_prompt_tokens(&summary) >= 1024,
"5000 reasoning-summary chars must meet a 1024-token floor"
);
let signature_only = IrRequest {
model: "claude".into(),
items: vec![IrItem::Reasoning {
encrypted: Some(long.clone()),
summary: None,
raw: None,
}],
tools: vec![],
sampling: IrSampling::default(),
};
assert!(
estimate_prompt_tokens(&signature_only) < 1024,
"a reasoning signature must not meet the floor by itself"
);
let raw_thinking = IrRequest {
model: "claude".into(),
items: vec![IrItem::Reasoning {
encrypted: None,
summary: None,
raw: Some(serde_json::json!({
"type": "thinking",
"thinking": long,
"signature": "y".repeat(5000),
})),
}],
tools: vec![],
sampling: IrSampling::default(),
};
assert!(
estimate_prompt_tokens(&raw_thinking) >= 1024,
"a raw thinking string must meet a 1024-token floor"
);
}
#[test]
fn new_and_retention_take_str_next_to_bytes() {
let _owned = bytes::Bytes::from_static(b"x");
let req = IrRequest::new("gpt-4o", vec![]).with_sampling(IrSampling::patch(|s| {
s.cache = IrCache::enabled().with_retention("5m");
}));
assert_eq!(req.model, "gpt-4o");
assert_eq!(req.sampling.cache.retention.as_deref(), Some("5m"));
let owned = IrRequest::new(String::from("gpt-4o"), vec![]);
assert_eq!(owned.model, "gpt-4o");
}
}