Files
zerogravity/src/api/types.rs
Nikketryhard 3303ce38de feat: add tool call support to chat completions endpoint
- 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)
2026-02-14 23:47:23 -06:00

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,
}