Appam
API Reference

UnifiedMessage

Provider-agnostic message format for cross-provider compatibility.

Overview

UnifiedMessage is the canonical, provider-agnostic message type used internally by Appam to abstract over differences between LLM provider APIs. The agent runtime operates solely on these unified types, enabling seamless provider switching without code changes.

All provider-specific formats (Anthropic Messages API, OpenAI Responses API, OpenRouter, Vertex Gemini) convert to and from UnifiedMessage transparently.

UnifiedMessage

pub struct UnifiedMessage {
    pub role: UnifiedRole,
    pub content: Vec<UnifiedContentBlock>,
    pub id: Option<String>,
    pub timestamp: Option<chrono::DateTime<chrono::Utc>>,
    pub reasoning: Option<String>,
    pub reasoning_details: Option<Vec<ReasoningDetail>>,
}

Fields

FieldTypeDescription
roleUnifiedRoleWho sent the message (System, User, or Assistant)
contentVec<UnifiedContentBlock>Array of typed content blocks (text, images, tool calls, etc.)
idOption<String>Optional provider-assigned message ID for conversation continuity
timestampOption<DateTime<Utc>>Optional timestamp of when the message was created
reasoningOption<String>Aggregated reasoning text, automatically preserved across multi-turn conversations
reasoning_detailsOption<Vec<ReasoningDetail>>Structured reasoning details for preservation across tool calls

Convenience Constructors

// Simple text message with any role
let msg = UnifiedMessage::text(UnifiedRole::User, "Hello!");

// Role-specific shortcuts
let user_msg = UnifiedMessage::user("What is Rust?");
let system_msg = UnifiedMessage::system("You are a helpful assistant.");
let assistant_msg = UnifiedMessage::assistant("Rust is a systems programming language.");

Extraction Methods

// Extract concatenated text from all Text content blocks
let text: String = message.extract_text();

// Extract all tool calls from the message
let calls: Vec<UnifiedToolCall> = message.extract_tool_calls();

// Check if the message contains any tool calls
let has_tools: bool = message.has_tool_calls();

// Extract concatenated reasoning/thinking content (None if no thinking blocks)
let reasoning: Option<String> = message.extract_reasoning();

UnifiedRole

pub enum UnifiedRole {
    System,
    User,
    Assistant,
}

Defines who sent the message. Unlike Role (used in ChatMessage), UnifiedRole does not include Tool or Developer -- tool results are represented as ToolResult content blocks within a User role message, and developer messages map to System.

UnifiedContentBlock

pub enum UnifiedContentBlock {
    Text { text: String },
    Image { source: ImageSource, detail: Option<String> },
    Document { source: DocumentSource, title: Option<String> },
    ToolUse { id: String, name: String, input: serde_json::Value },
    ToolResult { tool_use_id: String, content: serde_json::Value, is_error: Option<bool> },
    Thinking { thinking: String, signature: Option<String>, redacted: bool },
}

Variant Details

VariantDescription
TextPlain text content. The most common block type.
ImageImage data with source (base64 or URL) and an optional detail level for vision models.
DocumentDocument/PDF data (base64 PDF, URL PDF, or plain text) with an optional title.
ToolUseA tool invocation by the assistant. Contains the tool call id, the tool name, and the input arguments as a JSON value.
ToolResultResult from executing a tool. References the originating ToolUse by tool_use_id. The content field accepts string or structured JSON. is_error indicates failure.
ThinkingExtended thinking/reasoning content from models that expose their internal reasoning. For Anthropic, includes a cryptographic signature for verification. The redacted flag indicates encrypted or redacted thinking.

ImageSource

pub enum ImageSource {
    Base64 { media_type: String, data: String },
    Url { url: String },
}

Images can be provided as base64-encoded data (with media type like "image/jpeg" or "image/png") or as a URL.

DocumentSource

pub enum DocumentSource {
    Base64Pdf { media_type: String, data: String },
    UrlPdf { url: String },
    Text { media_type: String, data: String },
}

Documents support PDF (base64 or URL) and plain text formats.

Provider Mapping

The unified format maps to and from each provider's native format:

Anthropic to Unified

Anthropic BlockUnified Block
textText
imageImage
documentDocument
tool_useToolUse
tool_resultToolResult
thinkingThinking

OpenRouter / OpenAI to Unified

Responses API ItemUnified Block
message outputText
function_call outputToolUse
function_call_output inputToolResult
reasoning outputThinking

Vertex (Gemini) to Unified

Gemini PartUnified Block
textText
functionCallToolUse
functionResponseToolResult
thoughtThinking

UnifiedToolCall

pub struct UnifiedToolCall {
    pub id: String,
    pub name: String,
    pub input: serde_json::Value,
    pub raw_input_json: Option<String>,
}

Extracted from ToolUse content blocks via UnifiedMessage::extract_tool_calls(). The raw_input_json field preserves the original JSON string as provided by the provider (may be partial during streaming).

// Parse tool input into a typed struct
let args: MyToolArgs = tool_call.parse_input()?;

Example

use appam::llm::unified::*;
use serde_json::json;

// Build a multimodal assistant message with a tool call
let message = UnifiedMessage {
    role: UnifiedRole::Assistant,
    content: vec![
        UnifiedContentBlock::Thinking {
            thinking: "The user wants weather data. I should call the weather tool.".into(),
            signature: None,
            redacted: false,
        },
        UnifiedContentBlock::Text {
            text: "Let me check the weather for you.".into(),
        },
        UnifiedContentBlock::ToolUse {
            id: "call_abc123".into(),
            name: "get_weather".into(),
            input: json!({"location": "San Francisco, CA"}),
        },
    ],
    id: None,
    timestamp: Some(chrono::Utc::now()),
    reasoning: None,
    reasoning_details: None,
};

assert!(message.has_tool_calls());
assert_eq!(message.extract_tool_calls()[0].name, "get_weather");
assert_eq!(
    message.extract_reasoning().unwrap(),
    "The user wants weather data. I should call the weather tool."
);