#[derive(Clone, Debug, PartialEq)]
pub struct IrRequest {
pub model: String,
pub items: Vec<IrItem>,
pub tools: Vec<IrTool>,
pub sampling: IrSampling,
}
#[derive(Clone, Debug, Default, PartialEq)]
pub struct IrSampling {
pub temperature: Option<f32>,
pub top_p: Option<f32>,
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 include: Vec<String>,
pub prompt_cache_key: Option<String>,
pub service_tier: Option<String>,
}
#[derive(Clone, Debug, Default, Eq, PartialEq)]
pub enum IrToolChoice {
#[default]
Auto,
None,
Required,
Named(String),
}
#[derive(Clone, Debug, Default, Eq, PartialEq)]
pub struct IrCache {
pub enabled: bool,
pub retention: Option<String>,
pub min_cacheable_tokens: Option<u32>,
}
#[derive(Clone, Debug, PartialEq)]
pub enum IrItem {
System {
text: String,
},
Developer {
text: String,
},
User {
parts: Vec<IrPart>,
},
Assistant {
parts: Vec<IrPart>,
},
FunctionCall {
call_id: String,
name: String,
arguments: String,
thought_signature: Option<String>,
},
FunctionOutput {
call_id: String,
output: String,
},
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)]
pub enum IrPart {
Text(String),
ImageUrl(String),
ImageBase64 {
media_type: String,
data: String,
},
Thinking {
text: String,
signature: Option<String>,
},
Raw {
type_name: String,
raw: serde_json::Value,
},
}
#[derive(Clone, Debug, PartialEq)]
pub enum IrTool {
Function {
name: String,
description: String,
parameters: serde_json::Value,
},
Namespace {
name: String,
raw: serde_json::Value,
},
Hosted {
kind: String,
raw: serde_json::Value,
},
Unknown {
type_name: String,
raw: serde_json::Value,
},
}
#[derive(Clone, Debug, PartialEq)]
pub enum IrStreamEvent {
TextDelta {
text: String,
},
ReasoningDelta {
text: String,
},
ReasoningSignature {
signature: String,
},
ToolCallStart {
id: String,
name: String,
thought_signature: Option<String>,
},
ToolCallArgDelta {
delta: String,
},
ToolCallEnd,
Usage {
prompt_tokens: u32,
completion_tokens: u32,
cache_read_tokens: u32,
cache_write_tokens: u32,
reasoning_tokens: u32,
},
FinishReason {
reason: String,
},
Protocol {
item_type: String,
payload: serde_json::Value,
},
Unknown {
event: String,
raw: serde_json::Value,
},
Done,
}
#[derive(Clone, Debug, Default, Eq, PartialEq)]
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(),
});
}
}
#[derive(Clone, Debug, Eq, PartialEq)]
pub struct LossEvent {
pub path: String,
pub action: LossAction,
pub detail: String,
}
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
pub enum LossAction {
Preserve,
Degrade,
Drop,
HardError,
}
#[must_use]
pub fn estimate_prompt_tokens(req: &IrRequest) -> u32 {
let mut chars = 0usize;
for item in &req.items {
match item {
IrItem::System { text } | IrItem::Developer { text } => chars += text.len(),
IrItem::User { parts } | IrItem::Assistant { parts } => {
for part in parts {
match part {
IrPart::Text(text) | IrPart::Thinking { text, .. } => {
chars += text.len();
}
_ => {}
}
}
}
IrItem::FunctionCall {
name, arguments, ..
} => {
chars += name.len() + arguments.len();
}
IrItem::FunctionOutput { output, .. } => {
chars += output.len();
}
_ => {}
}
}
for tool in &req.tools {
if let IrTool::Function {
name,
description,
parameters,
} = tool
{
chars += name.len() + description.len() + parameters.to_string().len();
}
}
(chars / 4) as u32
}
#[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(),
},
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"}),
}],
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(),
};
let developer = IrItem::Developer {
text: "rules".into(),
};
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),
}],
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),
}],
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_thinking_text() {
let req = IrRequest {
model: "claude".into(),
items: vec![IrItem::Assistant {
parts: vec![IrPart::Thinking {
text: "x".repeat(5000),
signature: None,
}],
}],
tools: vec![],
sampling: IrSampling::default(),
};
assert!(
estimate_prompt_tokens(&req) >= 1024,
"5000 thinking chars must meet a 1024-token floor"
);
}
}