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 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_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>,
}
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,
},
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)]
#[non_exhaustive]
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,
},
Document {
source: IrDocumentSource,
media_type: String,
name: Option<String>,
},
Audio {
data: String,
format: String,
},
}
#[derive(Clone, Debug, PartialEq)]
#[non_exhaustive]
pub enum IrDocumentSource {
Base64(String),
Url(String),
FileId(String),
}
#[derive(Clone, Debug, PartialEq)]
#[non_exhaustive]
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)]
#[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,
},
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,
},
Created {
unix: i64,
},
ServiceTier {
tier: String,
},
Metadata {
metadata: BTreeMap<String, String>,
},
Moderation {
input: Option<serde_json::Value>,
output: Option<serde_json::Value>,
},
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,
},
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)]
#[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(),
});
}
}
#[derive(Clone, Debug, Eq, PartialEq)]
#[non_exhaustive]
pub struct LossEvent {
pub path: String,
pub action: LossAction,
pub detail: String,
}
#[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 {
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"
);
}
#[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");
}
}