- Accept tools and tool_choice fields in CompletionRequest - Convert OpenAI tools to Gemini format and store in MitmStore - Detect MITM-captured function calls in streaming poll loop - Emit tool_calls delta chunks in OpenAI streaming format - Finish with 'tool_calls' reason instead of 'stop' when tools used - Only clear tools when request has none (prevents stale state leak)
276 lines
8.4 KiB
Rust
276 lines
8.4 KiB
Rust
//! 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<String>,
|
|
pub input: serde_json::Value,
|
|
#[serde(default)]
|
|
pub instructions: Option<String>,
|
|
#[serde(default)]
|
|
pub stream: bool,
|
|
#[serde(default = "default_timeout")]
|
|
pub timeout: u64,
|
|
pub conversation: Option<serde_json::Value>,
|
|
#[serde(default = "default_true")]
|
|
pub store: bool,
|
|
#[serde(default)]
|
|
pub temperature: Option<f64>,
|
|
#[serde(default)]
|
|
pub top_p: Option<f64>,
|
|
#[serde(default)]
|
|
pub max_output_tokens: Option<u64>,
|
|
#[serde(default)]
|
|
pub previous_response_id: Option<String>,
|
|
#[serde(default)]
|
|
pub metadata: Option<serde_json::Value>,
|
|
#[serde(default)]
|
|
pub user: Option<String>,
|
|
/// Tool definitions (OpenAI format).
|
|
#[serde(default)]
|
|
pub tools: Option<Vec<serde_json::Value>>,
|
|
/// Tool choice: "auto", "required", "none", or {"type":"function","function":{"name":"X"}}.
|
|
#[serde(default)]
|
|
pub tool_choice: Option<serde_json::Value>,
|
|
}
|
|
|
|
/// Chat Completions request (OpenAI-compatible).
|
|
#[derive(Deserialize)]
|
|
pub(crate) struct CompletionRequest {
|
|
pub model: Option<String>,
|
|
pub messages: Vec<CompletionMessage>,
|
|
#[serde(default)]
|
|
pub stream: bool,
|
|
#[serde(default = "default_timeout")]
|
|
pub timeout: u64,
|
|
/// OpenAI-format tool definitions
|
|
pub tools: Option<Vec<serde_json::Value>>,
|
|
/// Tool choice: "auto", "none", "required", or {"type":"function","function":{"name":"..."}}
|
|
pub tool_choice: Option<serde_json::Value>,
|
|
}
|
|
|
|
#[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<u64>,
|
|
pub error: Option<serde_json::Value>,
|
|
pub incomplete_details: Option<serde_json::Value>,
|
|
pub instructions: Option<String>,
|
|
#[serde(serialize_with = "serialize_option_u64")]
|
|
pub max_output_tokens: Option<u64>,
|
|
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<serde_json::Value>,
|
|
pub parallel_tool_calls: bool,
|
|
pub previous_response_id: Option<String>,
|
|
pub reasoning: Reasoning,
|
|
pub store: bool,
|
|
pub temperature: f64,
|
|
pub text: TextFormat,
|
|
pub tool_choice: &'static str,
|
|
pub tools: Vec<serde_json::Value>,
|
|
pub top_p: f64,
|
|
pub truncation: &'static str,
|
|
pub usage: Option<Usage>,
|
|
pub user: Option<String>,
|
|
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<String>,
|
|
}
|
|
|
|
#[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<String>,
|
|
pub summary: Option<String>,
|
|
}
|
|
|
|
#[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<u64> as either the number or JSON null (not omitted).
|
|
fn serialize_option_u64<S>(val: &Option<u64>, s: S) -> Result<S::Ok, S::Error>
|
|
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,
|
|
}
|