feat: add Agents API, Conversations API, and multimodal support (v1.1.0)

Agents API (beta): create, get, update, delete, list agents with tools,
handoffs, completion args, and guardrails support.

Conversations API (beta): create, append, history, messages, restart,
delete, list conversations. Supports agent-backed and model-only
conversations with function calling and handoff execution modes.

Multimodal: ChatMessageContent enum (Text/Parts) with ContentPart
variants for text and image_url. Backwards-compatible constructors.
new_user_message_with_images() for mixed content messages.

Chat: reasoning field on ChatResponseChoice for Magistral models.
HTTP: PATCH methods for agent updates.

81 tests (30 live API integration + 35 serde unit + 16 existing).
This commit is contained in:
2026-03-21 20:58:25 +00:00
parent d5eb16dffc
commit a29c3c0109
15 changed files with 2721 additions and 16 deletions

View File

@@ -2,8 +2,9 @@ use serde::{Deserialize, Serialize};
use crate::v1::{chat, common, constants, tool};
// -----------------------------------------------------------------------------
// Request
// =============================================================================
// Agent Completions (existing — POST /v1/agents/completions)
// =============================================================================
#[derive(Debug)]
pub struct AgentCompletionParams {
@@ -84,9 +85,7 @@ impl AgentCompletionRequest {
}
}
// -----------------------------------------------------------------------------
// Response (same shape as chat completions)
// Agent completion response (same shape as chat completions)
#[derive(Clone, Debug, Deserialize, Serialize)]
pub struct AgentCompletionResponse {
pub id: String,
@@ -96,3 +95,189 @@ pub struct AgentCompletionResponse {
pub choices: Vec<chat::ChatResponseChoice>,
pub usage: common::ResponseUsage,
}
// =============================================================================
// Agents API — CRUD (Beta)
// POST/GET/PATCH/DELETE /v1/agents
// =============================================================================
// -----------------------------------------------------------------------------
// Tool types for agents
/// A function tool definition for an agent.
#[derive(Clone, Debug, Serialize, Deserialize)]
pub struct FunctionTool {
pub function: tool::ToolFunction,
}
/// Tool types available to agents.
#[derive(Clone, Debug, Serialize, Deserialize)]
#[serde(tag = "type")]
pub enum AgentTool {
#[serde(rename = "function")]
Function(FunctionTool),
#[serde(rename = "web_search")]
WebSearch {},
#[serde(rename = "web_search_premium")]
WebSearchPremium {},
#[serde(rename = "code_interpreter")]
CodeInterpreter {},
#[serde(rename = "image_generation")]
ImageGeneration {},
#[serde(rename = "document_library")]
DocumentLibrary {},
}
impl AgentTool {
/// Create a function tool from name, description, and JSON Schema parameters.
pub fn function(name: String, description: String, parameters: serde_json::Value) -> Self {
Self::Function(FunctionTool {
function: tool::ToolFunction {
name,
description,
parameters,
},
})
}
pub fn web_search() -> Self {
Self::WebSearch {}
}
pub fn code_interpreter() -> Self {
Self::CodeInterpreter {}
}
pub fn image_generation() -> Self {
Self::ImageGeneration {}
}
pub fn document_library() -> Self {
Self::DocumentLibrary {}
}
}
// -----------------------------------------------------------------------------
// Completion args (subset of chat params allowed for agents)
#[derive(Clone, Debug, Default, Serialize, Deserialize)]
pub struct CompletionArgs {
#[serde(skip_serializing_if = "Option::is_none")]
pub temperature: Option<f32>,
#[serde(skip_serializing_if = "Option::is_none")]
pub top_p: Option<f32>,
#[serde(skip_serializing_if = "Option::is_none")]
pub max_tokens: Option<u32>,
#[serde(skip_serializing_if = "Option::is_none")]
pub min_tokens: Option<u32>,
#[serde(skip_serializing_if = "Option::is_none")]
pub random_seed: Option<u32>,
#[serde(skip_serializing_if = "Option::is_none")]
pub stop: Option<Vec<String>>,
#[serde(skip_serializing_if = "Option::is_none")]
pub frequency_penalty: Option<f32>,
#[serde(skip_serializing_if = "Option::is_none")]
pub presence_penalty: Option<f32>,
#[serde(skip_serializing_if = "Option::is_none")]
pub response_format: Option<chat::ResponseFormat>,
#[serde(skip_serializing_if = "Option::is_none")]
pub tool_choice: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub prediction: Option<serde_json::Value>,
#[serde(skip_serializing_if = "Option::is_none")]
pub reasoning_effort: Option<String>,
}
// -----------------------------------------------------------------------------
// Create agent request
#[derive(Clone, Debug, Serialize, Deserialize)]
pub struct CreateAgentRequest {
pub model: String,
pub name: String,
#[serde(skip_serializing_if = "Option::is_none")]
pub description: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub instructions: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub tools: Option<Vec<AgentTool>>,
#[serde(skip_serializing_if = "Option::is_none")]
pub handoffs: Option<Vec<String>>,
#[serde(skip_serializing_if = "Option::is_none")]
pub completion_args: Option<CompletionArgs>,
#[serde(skip_serializing_if = "Option::is_none")]
pub metadata: Option<serde_json::Value>,
}
// -----------------------------------------------------------------------------
// Update agent request
#[derive(Clone, Debug, Default, Serialize, Deserialize)]
pub struct UpdateAgentRequest {
#[serde(skip_serializing_if = "Option::is_none")]
pub model: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub name: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub description: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub instructions: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub tools: Option<Vec<AgentTool>>,
#[serde(skip_serializing_if = "Option::is_none")]
pub handoffs: Option<Vec<String>>,
#[serde(skip_serializing_if = "Option::is_none")]
pub completion_args: Option<CompletionArgs>,
#[serde(skip_serializing_if = "Option::is_none")]
pub metadata: Option<serde_json::Value>,
}
// -----------------------------------------------------------------------------
// Agent response
#[derive(Clone, Debug, Deserialize, Serialize)]
pub struct Agent {
pub id: String,
pub object: String,
pub name: String,
pub model: String,
pub created_at: String,
pub updated_at: String,
#[serde(default)]
pub version: u64,
#[serde(default)]
pub versions: Vec<u64>,
#[serde(skip_serializing_if = "Option::is_none")]
pub description: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub instructions: Option<String>,
#[serde(default)]
pub tools: Vec<AgentTool>,
#[serde(skip_serializing_if = "Option::is_none")]
pub handoffs: Option<Vec<String>>,
#[serde(skip_serializing_if = "Option::is_none")]
pub completion_args: Option<CompletionArgs>,
#[serde(skip_serializing_if = "Option::is_none")]
pub metadata: Option<serde_json::Value>,
#[serde(default)]
pub deployment_chat: bool,
#[serde(skip_serializing_if = "Option::is_none")]
pub source: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub version_message: Option<String>,
#[serde(default)]
pub guardrails: Vec<serde_json::Value>,
}
/// List agents response. The API returns a raw JSON array of agents.
#[derive(Clone, Debug, Deserialize, Serialize)]
#[serde(transparent)]
pub struct AgentListResponse {
pub data: Vec<Agent>,
}
/// Delete agent response. The API returns 204 No Content on success.
#[derive(Clone, Debug)]
pub struct AgentDeleteResponse {
pub deleted: bool,
}

View File

@@ -2,13 +2,98 @@ use serde::{Deserialize, Serialize};
use crate::v1::{common, constants, tool};
// -----------------------------------------------------------------------------
// Content parts (multimodal)
/// A single part of a multimodal message.
#[derive(Clone, Debug, Deserialize, Serialize)]
#[serde(tag = "type")]
pub enum ContentPart {
#[serde(rename = "text")]
Text { text: String },
#[serde(rename = "image_url")]
ImageUrl {
image_url: ImageUrl,
},
}
#[derive(Clone, Debug, Deserialize, Serialize)]
pub struct ImageUrl {
pub url: String,
#[serde(skip_serializing_if = "Option::is_none")]
pub detail: Option<String>,
}
/// Message content: either a plain text string or multimodal content parts.
///
/// Serializes as a JSON string for text, or a JSON array for parts.
/// All existing `new_*_message()` constructors produce `Text` variants,
/// so existing code continues to work unchanged.
#[derive(Clone, Debug, Deserialize, Serialize)]
#[serde(untagged)]
pub enum ChatMessageContent {
Text(String),
Parts(Vec<ContentPart>),
}
impl ChatMessageContent {
/// Extract the text content. For multimodal messages, concatenates all text parts.
pub fn text(&self) -> String {
match self {
Self::Text(s) => s.clone(),
Self::Parts(parts) => parts
.iter()
.filter_map(|p| match p {
ContentPart::Text { text } => Some(text.as_str()),
_ => None,
})
.collect::<Vec<_>>()
.join(""),
}
}
/// Returns the content as a string slice if it is a plain text message.
pub fn as_text(&self) -> Option<&str> {
match self {
Self::Text(s) => Some(s),
Self::Parts(_) => None,
}
}
/// Returns true if this is a multimodal message with image parts.
pub fn has_images(&self) -> bool {
match self {
Self::Text(_) => false,
Self::Parts(parts) => parts.iter().any(|p| matches!(p, ContentPart::ImageUrl { .. })),
}
}
}
impl std::fmt::Display for ChatMessageContent {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(f, "{}", self.text())
}
}
impl From<String> for ChatMessageContent {
fn from(s: String) -> Self {
Self::Text(s)
}
}
impl From<&str> for ChatMessageContent {
fn from(s: &str) -> Self {
Self::Text(s.to_string())
}
}
// -----------------------------------------------------------------------------
// Definitions
#[derive(Clone, Debug, Deserialize, Serialize)]
pub struct ChatMessage {
pub role: ChatMessageRole,
pub content: String,
pub content: ChatMessageContent,
#[serde(skip_serializing_if = "Option::is_none")]
pub tool_calls: Option<Vec<tool::ToolCall>>,
/// Tool call ID, required when role is Tool.
@@ -22,7 +107,7 @@ impl ChatMessage {
pub fn new_system_message(content: &str) -> Self {
Self {
role: ChatMessageRole::System,
content: content.to_string(),
content: ChatMessageContent::Text(content.to_string()),
tool_calls: None,
tool_call_id: None,
name: None,
@@ -32,7 +117,7 @@ impl ChatMessage {
pub fn new_assistant_message(content: &str, tool_calls: Option<Vec<tool::ToolCall>>) -> Self {
Self {
role: ChatMessageRole::Assistant,
content: content.to_string(),
content: ChatMessageContent::Text(content.to_string()),
tool_calls,
tool_call_id: None,
name: None,
@@ -42,7 +127,18 @@ impl ChatMessage {
pub fn new_user_message(content: &str) -> Self {
Self {
role: ChatMessageRole::User,
content: content.to_string(),
content: ChatMessageContent::Text(content.to_string()),
tool_calls: None,
tool_call_id: None,
name: None,
}
}
/// Create a user message with mixed text and image content.
pub fn new_user_message_with_images(parts: Vec<ContentPart>) -> Self {
Self {
role: ChatMessageRole::User,
content: ChatMessageContent::Parts(parts),
tool_calls: None,
tool_call_id: None,
name: None,
@@ -52,7 +148,7 @@ impl ChatMessage {
pub fn new_tool_message(content: &str, tool_call_id: &str, name: Option<&str>) -> Self {
Self {
role: ChatMessageRole::Tool,
content: content.to_string(),
content: ChatMessageContent::Text(content.to_string()),
tool_calls: None,
tool_call_id: Some(tool_call_id.to_string()),
name: name.map(|n| n.to_string()),
@@ -238,6 +334,9 @@ pub struct ChatResponseChoice {
pub index: u32,
pub message: ChatMessage,
pub finish_reason: ChatResponseChoiceFinishReason,
/// Reasoning content returned by Magistral models.
#[serde(skip_serializing_if = "Option::is_none")]
pub reasoning: Option<String>,
}
#[derive(Clone, Debug, Eq, PartialEq, Deserialize, Serialize)]

View File

@@ -10,8 +10,8 @@ use std::{
};
use crate::v1::{
agents, audio, batch, chat, chat_stream, constants, embedding, error, files, fim, fine_tuning,
model_list, moderation, ocr, tool, utils,
agents, audio, batch, chat, chat_stream, constants, conversations, embedding, error, files,
fim, fine_tuning, model_list, moderation, ocr, tool, utils,
};
#[derive(Debug)]
@@ -900,6 +900,300 @@ impl Client {
.map_err(|e| self.to_api_error(e))
}
// =========================================================================
// Agents CRUD (Beta — /v1/agents)
// =========================================================================
pub fn create_agent(
&self,
request: &agents::CreateAgentRequest,
) -> Result<agents::Agent, error::ApiError> {
let response = self.post_sync("/agents", request)?;
response
.json::<agents::Agent>()
.map_err(|e| self.to_api_error(e))
}
pub async fn create_agent_async(
&self,
request: &agents::CreateAgentRequest,
) -> Result<agents::Agent, error::ApiError> {
let response = self.post_async("/agents", request).await?;
response
.json::<agents::Agent>()
.await
.map_err(|e| self.to_api_error(e))
}
pub fn get_agent(&self, agent_id: &str) -> Result<agents::Agent, error::ApiError> {
let response = self.get_sync(&format!("/agents/{}", agent_id))?;
response
.json::<agents::Agent>()
.map_err(|e| self.to_api_error(e))
}
pub async fn get_agent_async(
&self,
agent_id: &str,
) -> Result<agents::Agent, error::ApiError> {
let response = self.get_async(&format!("/agents/{}", agent_id)).await?;
response
.json::<agents::Agent>()
.await
.map_err(|e| self.to_api_error(e))
}
pub fn update_agent(
&self,
agent_id: &str,
request: &agents::UpdateAgentRequest,
) -> Result<agents::Agent, error::ApiError> {
let response = self.patch_sync(&format!("/agents/{}", agent_id), request)?;
response
.json::<agents::Agent>()
.map_err(|e| self.to_api_error(e))
}
pub async fn update_agent_async(
&self,
agent_id: &str,
request: &agents::UpdateAgentRequest,
) -> Result<agents::Agent, error::ApiError> {
let response = self
.patch_async(&format!("/agents/{}", agent_id), request)
.await?;
response
.json::<agents::Agent>()
.await
.map_err(|e| self.to_api_error(e))
}
pub fn delete_agent(
&self,
agent_id: &str,
) -> Result<agents::AgentDeleteResponse, error::ApiError> {
let _response = self.delete_sync(&format!("/agents/{}", agent_id))?;
Ok(agents::AgentDeleteResponse { deleted: true })
}
pub async fn delete_agent_async(
&self,
agent_id: &str,
) -> Result<agents::AgentDeleteResponse, error::ApiError> {
let _response = self
.delete_async(&format!("/agents/{}", agent_id))
.await?;
Ok(agents::AgentDeleteResponse { deleted: true })
}
pub fn list_agents(&self) -> Result<agents::AgentListResponse, error::ApiError> {
let response = self.get_sync("/agents")?;
response
.json::<agents::AgentListResponse>()
.map_err(|e| self.to_api_error(e))
}
pub async fn list_agents_async(
&self,
) -> Result<agents::AgentListResponse, error::ApiError> {
let response = self.get_async("/agents").await?;
response
.json::<agents::AgentListResponse>()
.await
.map_err(|e| self.to_api_error(e))
}
// =========================================================================
// Conversations (Beta — /v1/conversations)
// =========================================================================
pub fn create_conversation(
&self,
request: &conversations::CreateConversationRequest,
) -> Result<conversations::ConversationResponse, error::ApiError> {
let response = self.post_sync("/conversations", request)?;
response
.json::<conversations::ConversationResponse>()
.map_err(|e| self.to_api_error(e))
}
pub async fn create_conversation_async(
&self,
request: &conversations::CreateConversationRequest,
) -> Result<conversations::ConversationResponse, error::ApiError> {
let response = self.post_async("/conversations", request).await?;
response
.json::<conversations::ConversationResponse>()
.await
.map_err(|e| self.to_api_error(e))
}
pub fn append_conversation(
&self,
conversation_id: &str,
request: &conversations::AppendConversationRequest,
) -> Result<conversations::ConversationResponse, error::ApiError> {
let response =
self.post_sync(&format!("/conversations/{}", conversation_id), request)?;
response
.json::<conversations::ConversationResponse>()
.map_err(|e| self.to_api_error(e))
}
pub async fn append_conversation_async(
&self,
conversation_id: &str,
request: &conversations::AppendConversationRequest,
) -> Result<conversations::ConversationResponse, error::ApiError> {
let response = self
.post_async(&format!("/conversations/{}", conversation_id), request)
.await?;
response
.json::<conversations::ConversationResponse>()
.await
.map_err(|e| self.to_api_error(e))
}
pub fn get_conversation(
&self,
conversation_id: &str,
) -> Result<conversations::Conversation, error::ApiError> {
let response = self.get_sync(&format!("/conversations/{}", conversation_id))?;
response
.json::<conversations::Conversation>()
.map_err(|e| self.to_api_error(e))
}
pub async fn get_conversation_async(
&self,
conversation_id: &str,
) -> Result<conversations::Conversation, error::ApiError> {
let response = self
.get_async(&format!("/conversations/{}", conversation_id))
.await?;
response
.json::<conversations::Conversation>()
.await
.map_err(|e| self.to_api_error(e))
}
pub fn get_conversation_history(
&self,
conversation_id: &str,
) -> Result<conversations::ConversationHistoryResponse, error::ApiError> {
let response =
self.get_sync(&format!("/conversations/{}/history", conversation_id))?;
response
.json::<conversations::ConversationHistoryResponse>()
.map_err(|e| self.to_api_error(e))
}
pub async fn get_conversation_history_async(
&self,
conversation_id: &str,
) -> Result<conversations::ConversationHistoryResponse, error::ApiError> {
let response = self
.get_async(&format!("/conversations/{}/history", conversation_id))
.await?;
response
.json::<conversations::ConversationHistoryResponse>()
.await
.map_err(|e| self.to_api_error(e))
}
pub fn get_conversation_messages(
&self,
conversation_id: &str,
) -> Result<conversations::ConversationMessagesResponse, error::ApiError> {
let response =
self.get_sync(&format!("/conversations/{}/messages", conversation_id))?;
response
.json::<conversations::ConversationMessagesResponse>()
.map_err(|e| self.to_api_error(e))
}
pub async fn get_conversation_messages_async(
&self,
conversation_id: &str,
) -> Result<conversations::ConversationMessagesResponse, error::ApiError> {
let response = self
.get_async(&format!("/conversations/{}/messages", conversation_id))
.await?;
response
.json::<conversations::ConversationMessagesResponse>()
.await
.map_err(|e| self.to_api_error(e))
}
pub fn restart_conversation(
&self,
conversation_id: &str,
request: &conversations::RestartConversationRequest,
) -> Result<conversations::ConversationResponse, error::ApiError> {
let response = self.post_sync(
&format!("/conversations/{}/restart", conversation_id),
request,
)?;
response
.json::<conversations::ConversationResponse>()
.map_err(|e| self.to_api_error(e))
}
pub async fn restart_conversation_async(
&self,
conversation_id: &str,
request: &conversations::RestartConversationRequest,
) -> Result<conversations::ConversationResponse, error::ApiError> {
let response = self
.post_async(
&format!("/conversations/{}/restart", conversation_id),
request,
)
.await?;
response
.json::<conversations::ConversationResponse>()
.await
.map_err(|e| self.to_api_error(e))
}
pub fn delete_conversation(
&self,
conversation_id: &str,
) -> Result<conversations::ConversationDeleteResponse, error::ApiError> {
let _response =
self.delete_sync(&format!("/conversations/{}", conversation_id))?;
Ok(conversations::ConversationDeleteResponse { deleted: true })
}
pub async fn delete_conversation_async(
&self,
conversation_id: &str,
) -> Result<conversations::ConversationDeleteResponse, error::ApiError> {
let _response = self
.delete_async(&format!("/conversations/{}", conversation_id))
.await?;
Ok(conversations::ConversationDeleteResponse { deleted: true })
}
pub fn list_conversations(
&self,
) -> Result<conversations::ConversationListResponse, error::ApiError> {
let response = self.get_sync("/conversations")?;
response
.json::<conversations::ConversationListResponse>()
.map_err(|e| self.to_api_error(e))
}
pub async fn list_conversations_async(
&self,
) -> Result<conversations::ConversationListResponse, error::ApiError> {
let response = self.get_async("/conversations").await?;
response
.json::<conversations::ConversationListResponse>()
.await
.map_err(|e| self.to_api_error(e))
}
// =========================================================================
// Function Calling
// =========================================================================
@@ -1112,6 +1406,36 @@ impl Client {
self.handle_async_response(result).await
}
fn patch_sync<T: std::fmt::Debug + serde::ser::Serialize>(
&self,
path: &str,
params: &T,
) -> Result<reqwest::blocking::Response, error::ApiError> {
let reqwest_client = reqwest::blocking::Client::new();
let url = format!("{}{}", self.endpoint, path);
debug!("Request URL: {}", url);
utils::debug_pretty_json_from_struct("Request Body", params);
let request = self.build_request_sync(reqwest_client.patch(url).json(params));
let result = request.send();
self.handle_sync_response(result)
}
async fn patch_async<T: serde::ser::Serialize + std::fmt::Debug>(
&self,
path: &str,
params: &T,
) -> Result<reqwest::Response, error::ApiError> {
let reqwest_client = reqwest::Client::new();
let url = format!("{}{}", self.endpoint, path);
debug!("Request URL: {}", url);
utils::debug_pretty_json_from_struct("Request Body", params);
let request = self.build_request_async(reqwest_client.patch(url).json(params));
let result = request.send().await;
self.handle_async_response(result).await
}
fn delete_sync(&self, path: &str) -> Result<reqwest::blocking::Response, error::ApiError> {
let reqwest_client = reqwest::blocking::Client::new();
let url = format!("{}{}", self.endpoint, path);

377
src/v1/conversations.rs Normal file
View File

@@ -0,0 +1,377 @@
use serde::{Deserialize, Serialize};
use crate::v1::{agents, chat};
// =============================================================================
// Conversations API (Beta)
// POST/GET/DELETE /v1/conversations
// =============================================================================
// -----------------------------------------------------------------------------
// Conversation entries (inputs and outputs)
// All entries share common fields: id, object, type, created_at, completed_at
/// Input entry — a message sent to the conversation.
#[derive(Clone, Debug, Serialize, Deserialize)]
pub struct MessageInputEntry {
pub role: String,
pub content: chat::ChatMessageContent,
#[serde(skip_serializing_if = "Option::is_none")]
pub prefix: Option<bool>,
#[serde(skip_serializing_if = "Option::is_none")]
pub id: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub object: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub created_at: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub completed_at: Option<String>,
}
/// Output entry — an assistant message produced by the model.
#[derive(Clone, Debug, Serialize, Deserialize)]
pub struct MessageOutputEntry {
pub role: String,
pub content: chat::ChatMessageContent,
#[serde(skip_serializing_if = "Option::is_none")]
pub id: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub object: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub model: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub created_at: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub completed_at: Option<String>,
}
/// A function call requested by the model.
#[derive(Clone, Debug, Serialize, Deserialize)]
pub struct FunctionCallEntry {
pub name: String,
pub arguments: String,
#[serde(skip_serializing_if = "Option::is_none")]
pub id: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub object: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub tool_call_id: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub created_at: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub completed_at: Option<String>,
}
/// Result of a function call, sent back to the conversation.
#[derive(Clone, Debug, Serialize, Deserialize)]
pub struct FunctionResultEntry {
pub tool_call_id: String,
pub result: String,
#[serde(skip_serializing_if = "Option::is_none")]
pub id: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub object: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub created_at: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub completed_at: Option<String>,
}
/// A built-in tool execution (web_search, code_interpreter, etc.).
#[derive(Clone, Debug, Serialize, Deserialize)]
pub struct ToolExecutionEntry {
#[serde(skip_serializing_if = "Option::is_none")]
pub tool_name: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub id: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub object: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub created_at: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub completed_at: Option<String>,
#[serde(flatten)]
pub extra: serde_json::Value,
}
/// Agent handoff entry — transfer between agents.
#[derive(Clone, Debug, Serialize, Deserialize)]
pub struct AgentHandoffEntry {
#[serde(skip_serializing_if = "Option::is_none")]
pub previous_agent_id: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub next_agent_id: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub id: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub object: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub created_at: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub completed_at: Option<String>,
}
/// Union of all conversation entry types.
#[derive(Clone, Debug, Serialize, Deserialize)]
#[serde(tag = "type")]
pub enum ConversationEntry {
#[serde(rename = "message.input")]
MessageInput(MessageInputEntry),
#[serde(rename = "message.output")]
MessageOutput(MessageOutputEntry),
#[serde(rename = "function.call")]
FunctionCall(FunctionCallEntry),
#[serde(rename = "function.result")]
FunctionResult(FunctionResultEntry),
#[serde(rename = "tool.execution")]
ToolExecution(ToolExecutionEntry),
#[serde(rename = "agent.handoff")]
AgentHandoff(AgentHandoffEntry),
}
// -----------------------------------------------------------------------------
// Conversation inputs (flexible: string or array of entries)
/// Conversation input: either a plain string or structured entry array.
#[derive(Clone, Debug, Serialize, Deserialize)]
#[serde(untagged)]
pub enum ConversationInput {
Text(String),
Entries(Vec<ConversationEntry>),
}
impl From<&str> for ConversationInput {
fn from(s: &str) -> Self {
Self::Text(s.to_string())
}
}
impl From<String> for ConversationInput {
fn from(s: String) -> Self {
Self::Text(s)
}
}
impl From<Vec<ConversationEntry>> for ConversationInput {
fn from(entries: Vec<ConversationEntry>) -> Self {
Self::Entries(entries)
}
}
// -----------------------------------------------------------------------------
// Handoff execution mode
#[derive(Clone, Debug, Eq, PartialEq, Serialize, Deserialize)]
pub enum HandoffExecution {
#[serde(rename = "server")]
Server,
#[serde(rename = "client")]
Client,
}
impl Default for HandoffExecution {
fn default() -> Self {
Self::Server
}
}
// -----------------------------------------------------------------------------
// Create conversation request (POST /v1/conversations)
#[derive(Clone, Debug, Serialize, Deserialize)]
pub struct CreateConversationRequest {
pub inputs: ConversationInput,
#[serde(skip_serializing_if = "Option::is_none")]
pub model: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub agent_id: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub agent_version: Option<serde_json::Value>,
#[serde(skip_serializing_if = "Option::is_none")]
pub name: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub description: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub instructions: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub completion_args: Option<agents::CompletionArgs>,
#[serde(skip_serializing_if = "Option::is_none")]
pub tools: Option<Vec<agents::AgentTool>>,
#[serde(skip_serializing_if = "Option::is_none")]
pub handoff_execution: Option<HandoffExecution>,
#[serde(skip_serializing_if = "Option::is_none")]
pub metadata: Option<serde_json::Value>,
#[serde(skip_serializing_if = "Option::is_none")]
pub store: Option<bool>,
#[serde(default)]
pub stream: bool,
}
// -----------------------------------------------------------------------------
// Append to conversation request (POST /v1/conversations/{id})
#[derive(Clone, Debug, Serialize, Deserialize)]
pub struct AppendConversationRequest {
pub inputs: ConversationInput,
#[serde(skip_serializing_if = "Option::is_none")]
pub completion_args: Option<agents::CompletionArgs>,
#[serde(skip_serializing_if = "Option::is_none")]
pub handoff_execution: Option<HandoffExecution>,
#[serde(skip_serializing_if = "Option::is_none")]
pub store: Option<bool>,
#[serde(skip_serializing_if = "Option::is_none")]
pub tool_confirmations: Option<Vec<ToolCallConfirmation>>,
#[serde(default)]
pub stream: bool,
}
#[derive(Clone, Debug, Serialize, Deserialize)]
pub struct ToolCallConfirmation {
pub tool_call_id: String,
pub result: String,
}
// -----------------------------------------------------------------------------
// Restart conversation request (POST /v1/conversations/{id}/restart)
#[derive(Clone, Debug, Serialize, Deserialize)]
pub struct RestartConversationRequest {
pub from_entry_id: String,
#[serde(skip_serializing_if = "Option::is_none")]
pub inputs: Option<ConversationInput>,
#[serde(skip_serializing_if = "Option::is_none")]
pub completion_args: Option<agents::CompletionArgs>,
#[serde(skip_serializing_if = "Option::is_none")]
pub agent_version: Option<serde_json::Value>,
#[serde(skip_serializing_if = "Option::is_none")]
pub handoff_execution: Option<HandoffExecution>,
#[serde(skip_serializing_if = "Option::is_none")]
pub metadata: Option<serde_json::Value>,
#[serde(skip_serializing_if = "Option::is_none")]
pub store: Option<bool>,
#[serde(default)]
pub stream: bool,
}
// -----------------------------------------------------------------------------
// Conversation response (returned by create, append, restart)
#[derive(Clone, Debug, Deserialize, Serialize)]
pub struct ConversationUsageInfo {
#[serde(default)]
pub prompt_tokens: u32,
#[serde(default)]
pub completion_tokens: u32,
#[serde(default)]
pub total_tokens: u32,
}
#[derive(Clone, Debug, Deserialize, Serialize)]
pub struct ConversationResponse {
pub conversation_id: String,
pub outputs: Vec<ConversationEntry>,
pub usage: ConversationUsageInfo,
#[serde(default)]
pub object: String,
#[serde(skip_serializing_if = "Option::is_none")]
pub guardrails: Option<serde_json::Value>,
}
impl ConversationResponse {
/// Extract the assistant's text response from the outputs, if any.
pub fn assistant_text(&self) -> Option<String> {
for entry in &self.outputs {
if let ConversationEntry::MessageOutput(msg) = entry {
return Some(msg.content.text());
}
}
None
}
/// Extract all function call entries from the outputs.
pub fn function_calls(&self) -> Vec<&FunctionCallEntry> {
self.outputs
.iter()
.filter_map(|e| match e {
ConversationEntry::FunctionCall(fc) => Some(fc),
_ => None,
})
.collect()
}
/// Check if any outputs are agent handoff entries.
pub fn has_handoff(&self) -> bool {
self.outputs
.iter()
.any(|e| matches!(e, ConversationEntry::AgentHandoff(_)))
}
}
// -----------------------------------------------------------------------------
// Conversation history response (GET /v1/conversations/{id}/history)
#[derive(Clone, Debug, Deserialize, Serialize)]
pub struct ConversationHistoryResponse {
pub conversation_id: String,
pub entries: Vec<ConversationEntry>,
#[serde(default)]
pub object: String,
}
// -----------------------------------------------------------------------------
// Conversation messages response (GET /v1/conversations/{id}/messages)
// Note: may have same shape as history; keeping separate for API clarity
#[derive(Clone, Debug, Deserialize, Serialize)]
pub struct ConversationMessagesResponse {
pub conversation_id: String,
#[serde(alias = "messages", alias = "entries")]
pub messages: Vec<ConversationEntry>,
#[serde(default)]
pub object: String,
}
// -----------------------------------------------------------------------------
// Conversation info (GET /v1/conversations/{id})
#[derive(Clone, Debug, Deserialize, Serialize)]
pub struct Conversation {
pub id: String,
#[serde(default)]
pub object: String,
#[serde(skip_serializing_if = "Option::is_none")]
pub agent_id: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub model: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub name: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub description: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub instructions: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub created_at: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub updated_at: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub completion_args: Option<agents::CompletionArgs>,
#[serde(default)]
pub tools: Vec<agents::AgentTool>,
#[serde(skip_serializing_if = "Option::is_none")]
pub guardrails: Option<serde_json::Value>,
#[serde(skip_serializing_if = "Option::is_none")]
pub metadata: Option<serde_json::Value>,
}
/// List conversations response. API returns a raw JSON array.
#[derive(Clone, Debug, Deserialize, Serialize)]
#[serde(transparent)]
pub struct ConversationListResponse {
pub data: Vec<Conversation>,
}
/// Delete conversation response. API returns 204 No Content.
#[derive(Clone, Debug)]
pub struct ConversationDeleteResponse {
pub deleted: bool,
}

View File

@@ -6,6 +6,7 @@ pub mod chat_stream;
pub mod client;
pub mod common;
pub mod constants;
pub mod conversations;
pub mod embedding;
pub mod error;
pub mod files;