brazen 0.0.10

A stateless, swiss-army-knife adapter for every LLM provider and protocol.
Documentation
//! The canonical request model (§3.1): one authoritative request type every
//! protocol projects to and from. No IO. `Content` uses a custom serde repr
//! (CR-4) so a bare wire string (`"hi"`) and a `{"type":…}` object both decode
//! to it, and `content` fields accept a string, one object, or a sequence — that
//! wire projection lives in the sibling `request_de`. The closed value
//! vocabularies the LIFTED KNOBS carry (`OutputFormat`, `ReasoningEffort`,
//! `ServiceTier`) live in the sibling `knobs`.

use serde::{Deserialize, Serialize};
use serde_json::{Map, Value};

use super::knobs::{OutputFormat, ReasoningEffort, ServiceTier};

/// The single canonical request. A field set on the wire is used as-is; a field
/// it omits defaults (`getConfigValue` fills it later — §6.1). `extra` is the
/// long-tail valve: an unmodelled top-level key is forwarded verbatim.
#[derive(Clone, Debug, Default, PartialEq, Serialize, Deserialize)]
pub struct CanonicalRequest {
    /// Empty = absent: a request may omit `model` and let config supply it
    /// (`fill_absent`, §4.3/§4.4). An empty string and a missing key are the same
    /// "no model" fact — never two cases.
    #[serde(default)]
    pub model: String,
    #[serde(default)]
    pub system: Option<Vec<Content>>,
    #[serde(default)]
    pub messages: Vec<Message>,
    #[serde(default)]
    pub tools: Vec<Tool>,
    #[serde(default)]
    pub tool_choice: ToolChoice,
    /// May the model emit tool calls in parallel? `None` = provider default.
    /// A lifted known knob (architecture.md §3.1): both providers express it under
    /// different spellings (OpenAI top-level `parallel_tool_calls`, Anthropic nested
    /// `tool_choice.disable_parallel_tool_use`), so each adapter owns its projection.
    #[serde(default)]
    pub parallel_tool_calls: Option<bool>,
    #[serde(default)]
    pub max_tokens: Option<u32>,
    #[serde(default)]
    pub temperature: Option<f32>,
    #[serde(default)]
    pub top_p: Option<f32>,
    /// Portable reasoning EFFORT (architecture.md §3.1): a canonical `low|medium|high` each
    /// protocol maps to its native reasoning shape in `encode` — a lifted knob, NOT `extra`.
    /// `None` = none requested. Exact provider budgets stay reachable via the row's
    /// `body_defaults` (config §4.1), which the typed knob wins over on a same-named key.
    #[serde(default)]
    pub reasoning: Option<ReasoningEffort>,
    #[serde(default)]
    pub stop: Vec<String>,
    /// Wire-stream the response? `None` = absent, so `fill_absent` supplies it from config
    /// (`--stream`/`BRAZEN_STREAM`/file); a request that sets it wins, encoders read
    /// `unwrap_or(false)`. Request-shaping only — "stream over" is `Event::End` (arch §3.1).
    #[serde(default)]
    pub stream: Option<bool>,
    /// Portable STRUCTURED-OUTPUT intent (architecture.md §3.1): the FOURTH lifted knob —
    /// every dialect spells JSON-mode/JSON-schema differently (providers.md §6), so `encode`
    /// projects it, not `extra`. `None` = plain text; the typed knob wins over `body_defaults`/
    /// `extra`; a rejecting backend lists `output` in `unsupported_body_keys` (config §4.1.1).
    #[serde(default)]
    pub output: Option<OutputFormat>,
    /// Portable PROCESSING-LANE intent (architecture.md §3.1): the FIFTH lifted knob —
    /// priority processing is request SHAPING, so it is a typed field each `encode`
    /// projects (OpenAI `service_tier:"priority"`, Anthropic's asymmetric
    /// `"auto"|"standard_only"` — providers.md §6.2), never an `extra` key that could
    /// carry only one dialect's spelling. `None` = absent: the key is omitted and the
    /// provider's default lane applies. A backend that rejects the key lists
    /// `service_tier` in `unsupported_body_keys` (config §4.1.1).
    #[serde(default)]
    pub service_tier: Option<ServiceTier>,
    #[serde(flatten)]
    pub extra: Map<String, Value>,
}

impl CanonicalRequest {
    /// Resolve a tool call's function name from its `tool_use_id` (§4.5): scan
    /// every `Content::ToolUse` across messages for the matching `id`. The name is
    /// a fact that lives once, on the originating `ToolUse`; a `ToolResult`
    /// references it by id, so a NAME-keyed dialect (Google `functionResponse`,
    /// Ollama tool message) resolves it here rather than denormalizing a copy onto
    /// `ToolResult` (SSOT). `None` when the call is absent from this request (a bare
    /// tool-result turn) — the name is genuinely not in-band, so the consumer falls
    /// back (Google → the id, Ollama → omit `tool_name`).
    pub fn tool_name(&self, tool_use_id: &str) -> Option<&str> {
        self.messages
            .iter()
            .flat_map(|m| &m.content)
            .find_map(|c| match c {
                Content::ToolUse { id, name, .. } if id == tool_use_id => Some(name.as_str()),
                _ => None,
            })
    }
}

/// A transcript message. `content` is ALWAYS a `Vec<Content>`; a bare wire
/// string decodes to `vec![Text(..)]` (the string-vs-list distinction dies at
/// decode). Deserialize is hand-rolled in `request_de`: a `user` turn bearing any
/// `tool_result` decodes as `Role::Tool` (§2.2, the Anthropic packing normalized).
#[derive(Clone, Debug, PartialEq, Serialize)]
pub struct Message {
    pub role: Role,
    pub content: Vec<Content>,
}

#[derive(Clone, Copy, Debug, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "lowercase")]
#[non_exhaustive]
pub enum Role {
    System,
    User,
    Assistant,
    Tool,
}

/// A piece of content. `Text` is expressible as a bare string or a `{"type":"text",…}`
/// object; other variants are tagged objects. `Thinking`/`RedactedThinking` payloads and
/// the two server-tool variants (CR-4) round-trip verbatim — provider-executed blocks
/// carried untouched, mirroring the `RedactedThinking` rule.
#[derive(Clone, Debug, PartialEq)]
#[non_exhaustive]
pub enum Content {
    Text(String),
    Image {
        source: ImageSource,
    },
    /// A document/PDF, INPUT-ONLY, mirroring `Image` (no provider RETURNS one — no decode).
    Document {
        source: DocumentSource,
    },
    ToolUse {
        id: String,
        name: String,
        input: Value,
        /// Google `thoughtSignature` for this tool call — LOAD-BEARING for Gemini
        /// 2.5 multi-turn function calling (the API 400s if it is dropped on
        /// replay). `None` for dialects without it (Anthropic/OpenAI). Folded from
        /// a `Delta::SignatureDelta` on the tool block (bl-61a9).
        signature: Option<String>,
    },
    ToolResult {
        tool_use_id: String,
        content: Vec<Content>,
        is_error: bool,
    },
    Thinking {
        text: String,
        /// Anthropic thinking `signature` (the API 400s on an altered/absent one).
        signature: Option<String>,
        /// OpenAI Responses reasoning-item id (`rs_…`), echoed back on replay.
        id: Option<String>,
        /// OpenAI Responses `encrypted_content` — the encrypted reasoning payload
        /// for stateless (`store:false`) replay. `None` for dialects without it
        /// (bl-61a9).
        encrypted_content: Option<String>,
    },
    RedactedThinking {
        data: String,
    },
    /// Anthropic server-tool invocation (web_search etc.), opaque (CR-4). Echoed
    /// back VERBATIM on replay; never folded into `ToolUse`. id uses `srvtoolu_`.
    ServerToolUse {
        id: String,
        name: String,
        input: Value,
    },
    /// Server-tool RESULT, opaque. `kind` IS the wire tag (`web_search_tool_result`,
    /// `code_execution_tool_result`, …) — an open set carried as data, re-emitted
    /// verbatim on encode. `content` is the untouched provider payload: array of
    /// results on success, or `{type:*_error,...}` object on failure. Echoed back
    /// VERBATIM; NEVER a client `tool_result`.
    ServerToolResult {
        kind: String,
        tool_use_id: String,
        content: Value,
    },
}

#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
#[serde(tag = "kind", rename_all = "snake_case")]
pub enum ImageSource {
    Base64 { media_type: String, data: String },
    Url { url: String },
}

/// A document source — the `Image` analogue for PDFs/files, `kind`-tagged exactly like
/// `ImageSource`; a dialect that can't express a source rejects at encode (providers §9).
#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
#[serde(tag = "kind", rename_all = "snake_case")]
pub enum DocumentSource {
    Base64 { media_type: String, data: String },
    Url { url: String },
}

/// A declared tool — an OPEN SET (brazen enumerates none, registers none). The
/// enum distinguishes only NORMALIZE vs CARRY, and the harness declares which by
/// the shape it hands over: a wire object with no `type` key is `Custom`, one with
/// a `type` key is `Provider` (hand-rolled serde in `request_de`, keyed on `type`).
#[derive(Clone, Debug, PartialEq)] // hand-rolled serde in request_de, keyed on `type`
pub enum Tool {
    /// Caller-defined: brazen understands the STRUCTURE (name, description, JSON
    /// Schema) and PROJECTS it across dialects (Anthropic `input_schema` vs OpenAI
    /// `function.parameters`) — that projection is the whole value.
    Custom {
        name: String,
        description: Option<String>,
        input_schema: Value,
        /// OpenAI-style STRICT function calling (structured-output's per-tool sibling,
        /// architecture.md §3.1): `Some(true)` constrains the tool's `input` to the
        /// schema. A lifted known knob — nested inside the per-tool object each dialect
        /// spells differently (OpenAI chat `function.strict`, Responses flat `strict`,
        /// Anthropic tool `strict`), so `extra` cannot reach it. `None` = provider
        /// default; Google/Ollama LACK the field → a documented narrowing (providers §6).
        strict: Option<bool>,
    },
    /// Provider-typed (Anthropic-schema client tools bash/computer/… AND server
    /// tools web_search/…): opaque `kind` (wire `type`, e.g. `web_search_20250305`)
    /// plus config, passed through to the routed provider verbatim. brazen has no
    /// opinion on `kind`; the provider is the authority (a bad one → provider 400).
    Provider {
        kind: String,
        name: String,
        config: Map<String, Value>,
    },
}

/// All four tool-use intents, lifted explicitly rather than left in `extra`.
#[derive(Clone, Debug, Default, PartialEq, Serialize, Deserialize)]
#[serde(tag = "type", rename_all = "snake_case")]
pub enum ToolChoice {
    #[default]
    Auto,
    Any,
    Tool {
        name: String,
    },
    None,
}