//! Request/response types for the OpenAI-compatible API. //! //! All response shapes strictly match the official OpenAI Responses API spec: //! https://platform.openai.com/docs/api-reference/responses use serde::{Deserialize, Serialize}; // ─── Request types ─────────────────────────────────────────────────────────── #[derive(Deserialize)] pub(crate) struct ResponsesRequest { pub model: Option, pub input: serde_json::Value, #[serde(default)] pub instructions: Option, #[serde(default)] pub stream: bool, #[serde(default = "default_timeout")] pub timeout: u64, pub conversation: Option, #[serde(default = "default_true")] pub store: bool, #[serde(default)] pub temperature: Option, #[serde(default)] pub top_p: Option, #[serde(default)] pub max_output_tokens: Option, #[serde(default)] pub previous_response_id: Option, #[serde(default)] pub metadata: Option, #[serde(default)] pub user: Option, /// Tool definitions (OpenAI format). #[serde(default)] pub tools: Option>, /// Tool choice: "auto", "required", "none", or {"type":"function","function":{"name":"X"}}. #[serde(default)] pub tool_choice: Option, } /// Chat Completions request (OpenAI-compatible). #[derive(Deserialize)] pub(crate) struct CompletionRequest { pub model: Option, pub messages: Vec, #[serde(default)] pub stream: bool, #[serde(default = "default_timeout")] pub timeout: u64, /// OpenAI-format tool definitions pub tools: Option>, /// Tool choice: "auto", "none", "required", or {"type":"function","function":{"name":"..."}} pub tool_choice: Option, } #[derive(Deserialize)] pub(crate) struct CompletionMessage { pub role: String, pub content: serde_json::Value, } fn default_timeout() -> u64 { 120 } fn default_true() -> bool { true } // ─── Response types (official OpenAI Responses API shape) ──────────────────── /// Top-level Response object — matches OpenAI exactly. #[derive(Serialize, Clone)] pub(crate) struct ResponsesResponse { pub id: String, pub object: &'static str, pub created_at: u64, pub status: &'static str, #[serde(serialize_with = "serialize_option_u64")] pub completed_at: Option, pub error: Option, pub incomplete_details: Option, pub instructions: Option, #[serde(serialize_with = "serialize_option_u64")] pub max_output_tokens: Option, pub model: String, /// Output items — can contain both `"reasoning"` and `"message"` items. /// Uses serde_json::Value because reasoning and message items have different shapes. pub output: Vec, pub parallel_tool_calls: bool, pub previous_response_id: Option, pub reasoning: Reasoning, pub store: bool, pub temperature: f64, pub text: TextFormat, pub tool_choice: &'static str, pub tools: Vec, pub top_p: f64, pub truncation: &'static str, pub usage: Option, pub user: Option, pub metadata: serde_json::Value, /// Proxy extension: opaque thinking verification signature. /// Required for multi-turn chaining with thinking models. /// Not part of the official OpenAI spec — internal proxy data. #[serde(skip_serializing_if = "Option::is_none")] pub thinking_signature: Option, } #[derive(Serialize, Clone)] pub(crate) struct Usage { pub input_tokens: u64, pub input_tokens_details: InputTokensDetails, pub output_tokens: u64, pub output_tokens_details: OutputTokensDetails, pub total_tokens: u64, } #[derive(Serialize, Clone)] pub(crate) struct InputTokensDetails { pub cached_tokens: u64, } #[derive(Serialize, Clone)] pub(crate) struct OutputTokensDetails { pub reasoning_tokens: u64, } #[derive(Serialize, Clone)] #[derive(Default)] pub(crate) struct Reasoning { pub effort: Option, pub summary: Option, } #[derive(Serialize, Clone)] pub(crate) struct TextFormat { pub format: TextFormatInner, } #[derive(Serialize, Clone)] pub(crate) struct TextFormatInner { #[serde(rename = "type")] pub format_type: &'static str, } impl Usage { /// Estimate token counts from actual text. /// Uses ~4 chars/token heuristic (standard GPT tokenizer average). pub fn estimate(input_text: &str, output_text: &str) -> Self { let input_tokens = (input_text.len() as u64).div_ceil(4); let output_tokens = (output_text.len() as u64).div_ceil(4); Self { input_tokens, input_tokens_details: InputTokensDetails { cached_tokens: 0 }, output_tokens, output_tokens_details: OutputTokensDetails { reasoning_tokens: 0, }, total_tokens: input_tokens + output_tokens, } } } impl Default for Usage { fn default() -> Self { Self { input_tokens: 0, input_tokens_details: InputTokensDetails { cached_tokens: 0 }, output_tokens: 0, output_tokens_details: OutputTokensDetails { reasoning_tokens: 0, }, total_tokens: 0, } } } impl Default for TextFormat { fn default() -> Self { Self { format: TextFormatInner { format_type: "text", }, } } } // ─── Output item builders ──────────────────────────────────────────────────── /// Build a reasoning output item (goes BEFORE the message item in `output`). /// Matches: https://platform.openai.com/docs/api-reference/responses pub fn build_reasoning_output(thinking_text: &str) -> serde_json::Value { serde_json::json!({ "id": format!("rs_{}", uuid::Uuid::new_v4().to_string().replace('-', "")), "type": "reasoning", "summary": [{ "type": "summary_text", "text": thinking_text, }], }) } /// Build a message output item. pub fn build_message_output(msg_id: &str, text: &str) -> serde_json::Value { serde_json::json!({ "type": "message", "id": msg_id, "status": "completed", "role": "assistant", "content": [{ "type": "output_text", "text": text, "annotations": [], }], }) } /// Build an in-progress message output item (no content yet, for streaming). pub fn build_message_output_in_progress(msg_id: &str) -> serde_json::Value { serde_json::json!({ "type": "message", "id": msg_id, "status": "in_progress", "role": "assistant", "content": [], }) } /// Build a function_call output item (OpenAI Responses API format). pub fn build_function_call_output(call_id: &str, name: &str, arguments: &str) -> serde_json::Value { serde_json::json!({ "type": "function_call", "id": call_id, "call_id": call_id, "name": name, "arguments": arguments, "status": "completed", }) } // ─── Helpers ───────────────────────────────────────────────────────────────── /// Serialize Option as either the number or JSON null (not omitted). fn serialize_option_u64(val: &Option, s: S) -> Result where S: serde::Serializer, { match val { Some(v) => s.serialize_u64(*v), None => s.serialize_none(), } } // ─── Shared types ──────────────────────────────────────────────────────────── #[derive(Deserialize)] pub(crate) struct TokenRequest { pub token: String, } #[derive(Serialize)] pub(crate) struct ErrorResponse { pub error: ErrorDetail, } #[derive(Serialize)] pub(crate) struct ErrorDetail { pub message: String, #[serde(rename = "type")] pub error_type: String, }