#![cfg_attr(feature = "fail-on-warnings", deny(warnings))]
#![warn(clippy::all, clippy::pedantic, clippy::nursery, clippy::cargo)]
#![allow(clippy::multiple_crate_versions)]
mod usage_decoder;
pub use usage_decoder::ResponsesUsageDialect;
pub mod usage;
use serde::{Deserialize, Serialize};
pub use decode::{
ReasoningItemAccumulator, ResponsesStreamLine, ToolCallAccumulator,
classify_responses_stream_line, drain_complete_stream_lines, ensure_reasoning_activity_started,
parse_tool_arguments, process_responses_function_arguments_delta,
process_responses_function_arguments_done, process_responses_output_item,
process_responses_reasoning_delta, process_responses_reasoning_done,
process_responses_reasoning_output_item, reasoning_output_index, reported_output_index,
responses_event_type, responses_incomplete_reason, responses_output_item_text,
responses_text_delta, tool_call_output_index,
};
mod decode;
mod projection;
pub use projection::{
INTERRUPTED_TOOL_OUTPUT, append_missing_responses_tool_outputs, image_data_url, image_ref_text,
joined_text_content, model_message_to_responses_input, model_messages_to_responses_input,
model_messages_to_responses_input_with_cache_boundaries, push_sanitized_responses_input_item,
response_instruction_bundle, responses_assistant_items, responses_message,
responses_tool_items,
};
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum StreamOutcome {
Finished,
ToolCall,
MaxTokens,
Cancelled,
}
impl StreamOutcome {
#[must_use]
pub const fn is_provider_terminal(self) -> bool {
matches!(self, Self::Finished | Self::ToolCall | Self::MaxTokens)
}
}
pub trait ResponsesEventSink {
fn push(&self, event: bcode_model::ProviderTurnEvent);
}
impl<T> ResponsesEventSink for &T
where
T: ResponsesEventSink + ?Sized,
{
fn push(&self, event: bcode_model::ProviderTurnEvent) {
(**self).push(event);
}
}
#[allow(clippy::struct_excessive_bools)]
#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
pub struct ResponsesRequestCapabilities {
pub supports_previous_response_id: bool,
pub supports_parallel_tool_calls: bool,
pub supports_prompt_cache_key: bool,
pub supports_max_output_tokens: bool,
pub supports_text_verbosity: bool,
pub supports_reasoning_context: bool,
pub requires_explicit_tool_strictness: bool,
pub replays_provider_reasoning_items: bool,
pub projects_reused_history: bool,
}
impl ResponsesRequestCapabilities {
#[must_use]
pub const fn public_responses_api() -> Self {
Self {
supports_previous_response_id: true,
supports_parallel_tool_calls: true,
supports_prompt_cache_key: false,
supports_max_output_tokens: true,
supports_text_verbosity: false,
supports_reasoning_context: false,
requires_explicit_tool_strictness: true,
replays_provider_reasoning_items: false,
projects_reused_history: true,
}
}
}
#[derive(Debug, Serialize)]
pub struct ResponsesRequest {
pub model: String,
#[serde(skip_serializing_if = "Option::is_none")]
pub instructions: Option<String>,
pub input: Vec<ResponsesInputItem>,
pub stream: bool,
pub store: bool,
#[serde(skip_serializing_if = "Option::is_none")]
pub previous_response_id: Option<String>,
#[serde(skip_serializing_if = "Vec::is_empty")]
pub context_management: Vec<ResponsesContextManagement>,
#[serde(skip_serializing_if = "Vec::is_empty")]
pub tools: Vec<ResponsesTool>,
#[serde(skip_serializing_if = "Option::is_none")]
pub tool_choice: Option<serde_json::Value>,
#[serde(skip_serializing_if = "Option::is_none")]
pub parallel_tool_calls: Option<bool>,
#[serde(skip_serializing_if = "Option::is_none")]
pub text: Option<ResponsesTextOptions>,
#[serde(skip_serializing_if = "Option::is_none")]
pub reasoning: Option<ResponsesReasoningOptions>,
#[serde(skip_serializing_if = "Vec::is_empty")]
pub include: Vec<&'static str>,
#[serde(skip_serializing_if = "Option::is_none")]
pub prompt_cache_key: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub prompt_cache_options: Option<ResponsesPromptCacheOptions>,
#[serde(skip_serializing_if = "Option::is_none")]
pub temperature: Option<f32>,
#[serde(skip_serializing_if = "Option::is_none")]
pub max_output_tokens: Option<u32>,
#[serde(skip_serializing_if = "Option::is_none")]
pub top_p: Option<f32>,
}
#[derive(Debug, Serialize)]
pub struct ResponsesPromptCacheOptions {
pub mode: &'static str,
#[serde(skip_serializing_if = "Option::is_none")]
pub ttl: Option<&'static str>,
}
#[derive(Debug, Serialize, Deserialize)]
pub struct ResponsesPromptCacheBreakpoint {
pub mode: String,
}
#[derive(Debug, Serialize)]
pub struct ResponsesContextManagement {
pub r#type: &'static str,
pub compact_threshold: u64,
}
#[derive(Debug, Serialize)]
pub struct ResponsesReasoningOptions {
#[serde(skip_serializing_if = "Option::is_none")]
pub effort: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub summary: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub context: Option<&'static str>,
}
#[derive(Debug, Serialize)]
pub struct ResponsesTextOptions {
#[serde(skip_serializing_if = "Option::is_none")]
pub format: Option<ResponsesTextFormat>,
pub verbosity: &'static str,
}
#[derive(Debug, Serialize)]
pub struct ResponsesTextFormat {
pub r#type: &'static str,
pub name: String,
pub schema: serde_json::Value,
pub strict: bool,
}
#[derive(Debug, Serialize, Deserialize)]
#[serde(tag = "type", rename_all = "snake_case")]
pub enum ResponsesInputItem {
Message {
role: String,
content: Vec<ResponsesContent>,
},
FunctionCall {
call_id: String,
name: String,
arguments: String,
},
FunctionCallOutput {
call_id: String,
output: String,
},
Reasoning {
id: String,
#[serde(default)]
summary: Vec<ResponsesReasoningSummary>,
encrypted_content: String,
},
Compaction {
id: String,
encrypted_content: String,
#[serde(default, skip_serializing_if = "Option::is_none")]
created_by: Option<String>,
},
}
#[derive(Debug, Serialize, Deserialize)]
#[serde(tag = "type", rename_all = "snake_case")]
pub enum ResponsesReasoningSummary {
SummaryText {
text: String,
},
}
#[derive(Debug, Serialize, Deserialize)]
#[serde(tag = "type", rename_all = "snake_case")]
pub enum ResponsesContent {
InputText {
text: String,
#[serde(default, skip_serializing_if = "Option::is_none")]
prompt_cache_breakpoint: Option<ResponsesPromptCacheBreakpoint>,
},
OutputText {
text: String,
},
InputImage {
image_url: String,
},
}
#[derive(Debug, Serialize)]
pub struct ResponsesTool {
pub r#type: &'static str,
pub name: String,
pub description: String,
pub parameters: serde_json::Value,
pub strict: Option<bool>,
}
#[derive(Debug, Deserialize)]
pub struct ResponsesNativeSearchBody {
#[serde(default)]
pub output: Vec<ResponsesNativeSearchOutputItem>,
}
#[derive(Debug, Deserialize)]
pub struct ResponsesNativeSearchOutputItem {
#[serde(default)]
pub content: Vec<ResponsesNativeSearchContentItem>,
}
#[derive(Debug, Deserialize)]
pub struct ResponsesNativeSearchContentItem {
#[serde(default)]
pub text: Option<String>,
#[serde(default)]
pub annotations: Vec<ResponsesNativeSearchAnnotation>,
}
#[derive(Debug, Deserialize)]
pub struct ResponsesNativeSearchAnnotation {
#[serde(default)]
pub r#type: String,
#[serde(default)]
pub title: Option<String>,
#[serde(default)]
pub url: Option<String>,
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn only_provider_reported_outcomes_are_terminal() {
assert!(StreamOutcome::Finished.is_provider_terminal());
assert!(StreamOutcome::ToolCall.is_provider_terminal());
assert!(StreamOutcome::MaxTokens.is_provider_terminal());
assert!(!StreamOutcome::Cancelled.is_provider_terminal());
}
#[test]
fn event_sink_is_implemented_for_references_and_receives_events_in_order() {
struct Recorder {
events: std::cell::RefCell<Vec<bcode_model::ProviderTurnEvent>>,
}
impl ResponsesEventSink for Recorder {
fn push(&self, event: bcode_model::ProviderTurnEvent) {
self.events.borrow_mut().push(event);
}
}
fn emit(sink: &impl ResponsesEventSink) {
sink.push(bcode_model::ProviderTurnEvent::TextDelta {
text: "a".to_string(),
});
sink.push(bcode_model::ProviderTurnEvent::TextDelta {
text: "b".to_string(),
});
}
let recorder = Recorder {
events: std::cell::RefCell::new(Vec::new()),
};
emit(&&recorder);
let events = recorder.events.borrow();
assert_eq!(events.len(), 2);
assert!(matches!(
&events[0],
bcode_model::ProviderTurnEvent::TextDelta { text } if text == "a"
));
assert!(matches!(
&events[1],
bcode_model::ProviderTurnEvent::TextDelta { text } if text == "b"
));
}
#[test]
fn unset_optional_request_fields_are_omitted_entirely() {
let request = ResponsesRequest {
model: "test-model".to_string(),
instructions: None,
input: Vec::new(),
stream: true,
store: false,
previous_response_id: None,
context_management: Vec::new(),
tools: Vec::new(),
tool_choice: None,
parallel_tool_calls: None,
text: None,
reasoning: None,
include: Vec::new(),
prompt_cache_key: None,
prompt_cache_options: None,
temperature: None,
max_output_tokens: None,
top_p: None,
};
let value = serde_json::to_value(&request).expect("serialize request");
let object = value.as_object().expect("request is an object");
for field in [
"instructions",
"previous_response_id",
"context_management",
"tools",
"tool_choice",
"parallel_tool_calls",
"text",
"reasoning",
"include",
"prompt_cache_key",
"temperature",
"max_output_tokens",
"top_p",
] {
assert!(!object.contains_key(field), "{field} must be omitted");
}
assert_eq!(
object.get("model").and_then(serde_json::Value::as_str),
Some("test-model")
);
assert_eq!(
object.get("stream").and_then(serde_json::Value::as_bool),
Some(true)
);
assert_eq!(
object.get("store").and_then(serde_json::Value::as_bool),
Some(false)
);
assert!(object.contains_key("input"));
}
#[test]
fn input_items_use_type_tagged_snake_case_wire_names() {
let items = vec![
ResponsesInputItem::Message {
role: "user".to_string(),
content: vec![ResponsesContent::InputText {
text: "hi".to_string(),
prompt_cache_breakpoint: None,
}],
},
ResponsesInputItem::FunctionCall {
call_id: "call_1".to_string(),
name: "read".to_string(),
arguments: "{}".to_string(),
},
ResponsesInputItem::FunctionCallOutput {
call_id: "call_1".to_string(),
output: "done".to_string(),
},
ResponsesInputItem::Reasoning {
id: "rs_1".to_string(),
summary: vec![ResponsesReasoningSummary::SummaryText {
text: "thinking".to_string(),
}],
encrypted_content: "opaque".to_string(),
},
ResponsesInputItem::Compaction {
id: "cp_1".to_string(),
encrypted_content: "opaque".to_string(),
created_by: None,
},
];
let value = serde_json::to_value(&items).expect("serialize input items");
let encoded = value.as_array().expect("input items are an array");
let types = encoded
.iter()
.map(|item| item.get("type").and_then(serde_json::Value::as_str))
.collect::<Vec<_>>();
assert_eq!(
types,
vec![
Some("message"),
Some("function_call"),
Some("function_call_output"),
Some("reasoning"),
Some("compaction"),
]
);
assert!(
!encoded[4]
.as_object()
.expect("compaction item is an object")
.contains_key("created_by")
);
let decoded: Vec<ResponsesInputItem> =
serde_json::from_value(value).expect("input items round-trip");
assert_eq!(decoded.len(), items.len());
}
#[test]
fn content_parts_use_documented_wire_names() {
let value = serde_json::to_value(vec![
ResponsesContent::InputText {
text: "a".to_string(),
prompt_cache_breakpoint: None,
},
ResponsesContent::OutputText {
text: "b".to_string(),
},
ResponsesContent::InputImage {
image_url: "https://example.invalid/i.png".to_string(),
},
])
.expect("serialize content");
let types = value
.as_array()
.expect("content is an array")
.iter()
.map(|item| item.get("type").and_then(serde_json::Value::as_str))
.collect::<Vec<_>>();
assert_eq!(
types,
vec![Some("input_text"), Some("output_text"), Some("input_image")]
);
}
#[test]
fn native_search_body_tolerates_absent_collections() {
let body: ResponsesNativeSearchBody =
serde_json::from_str("{}").expect("empty body decodes");
assert!(body.output.is_empty());
let body: ResponsesNativeSearchBody =
serde_json::from_str(r#"{"output":[{"content":[{"text":"t"}]}]}"#)
.expect("body decodes");
assert_eq!(body.output.len(), 1);
assert_eq!(body.output[0].content.len(), 1);
assert_eq!(body.output[0].content[0].text.as_deref(), Some("t"));
assert!(body.output[0].content[0].annotations.is_empty());
}
}