Skip to content

LLM-Related Protocols

An LLM request describes a conversation, the tools available to the model, and generation settings. Switchyard represents these in LlmRequest. The same type works whether the next call goes to Anthropic Messages, OpenAI Responses, or Chat Completions.

Fields, API mappings, and conversion rules

Read a request

LlmRequest separates instructions from conversation turns. A Message contains a role and ordered ContentBlock values. Content can include text, media, reasoning, tool calls, and tool results.

This example constructs the neutral request passed to an algorithm. It uses the quickstart's dependencies.

use switchyard_protocol::{ContentBlock, InstructionBlock, LlmRequest, Message, Request, Role};

let request = Request {
    llm_request: LlmRequest {
        instructions: vec![InstructionBlock {
            role: Role::System,
            content: vec![ContentBlock::Text { text: "Answer briefly.".into() }],
        }],
        messages: vec![Message::text(Role::User, "Explain tail latency.")],
        ..LlmRequest::default()
    },
    ..Request::default()
};

Request is the host-facing envelope. It adds optional correlation metadata and raw_request, a host-owned JSON body. Codecs use LlmRequest::preservation for replay, not Request::raw_request.

Read a response

The host returns a Response envelope containing LlmResponse. LlmResponse::Agg holds a completed AggLlmResponse. LlmResponse::Stream holds a single-consumption stream of LlmResponseStreamEvent values. Each stream event exposes neutral chunks through normalized(). Chunks describe message starts, text deltas, tool arguments, usage, and completion. An event may also retain parsed provider JSON for same-format replay.

Usage::input_tokens counts non-cached input. The codecs separate cache-read and cache-write tokens from OpenAI's aggregate input counts. Missing counts stay None. Call LlmResponse::into_agg().await when you need a buffered result; this consumes the stream and aggregates its normalized chunks.

Understand lossless conversion

The protocol retains source data for lossless same-format round trips. With the default preservation policy, decoding a supported API body and encoding it back to that API reuses its preserved JSON. The preservation reference explains exactly what is retained and when replay takes precedence.

Conversion between different provider APIs can be lossy. For example, Anthropic has no separate developer role, and Chat Completions has no native equivalent for every Responses built-in tool. Keeping the original data in the protocol does not give the destination API those capabilities.

Translation reports known losses in diagnostics. The translation policy reference explains how to allow or reject these conversions.

Encoding an edited request

A preserved same-format body takes precedence over normalized fields. If you change the protocol fields and want those edits encoded, remove that format's entry from preservation.requests, or encode with PreservationPolicy::Disabled. The same rule applies to preservation.responses for buffered responses.

See stream preservation and aggregation for how replay and edits work on streamed responses.

Use the translation crate

switchyard-translation supplies the LLM codecs without making network calls. Add it at the same API revision as libsy and switchyard-protocol, following the quickstart's translation setup.

This excerpt decodes an inbound Chat Completions body and encodes it as an OpenAI Responses request. It can run inside your adapter; outbound.body is the JSON your client sends.

use serde_json::json;
use switchyard_translation::{TranslationEngine, TranslationPolicy, WireFormat};

let engine = TranslationEngine::default();
let policy = TranslationPolicy::default();
let inbound = json!({
    "model": "your-model",
    "messages": [{"role": "user", "content": "Explain tail latency."}]
});

let decoded = engine.decode_request(WireFormat::OpenAiChat, &inbound, &policy)?;
let outbound = engine.encode_request(
    WireFormat::OpenAiResponses,
    &decoded.request,
    &policy,
)?;
// Check diagnostics from both operations before sending the encoded body.

During a libsy run, place decoded.request in Request::llm_request. Encode the request from Step::CallModel for the selected backend. Decode its reply with decode_response, or use decode_stream_event and encode_stream_event with a separate StreamTranslationState per response stream. Return the neutral Response through the call object, as shown in Mechanics of Libsy.

The engine also exposes translate_request and translate_response for direct API-to-API conversion. See the translation engine reference for signatures and results.

Keep your own client or conversions

You can construct these protocol types from your existing SDK and convert them back yourself. When handling Step::CallModel, pass call.respond a future that uses your client and returns libsy::Result<Response>. Keep content order, tool IDs, provider-only data, and streaming lifetime intact where your integration needs them.

RoutedLlmClient is an optional shared interface used by Switchyard's clients and execution helpers. Direct run_stream handling works with your own functions or clients; it does not require implementing that trait.

If you want Switchyard to handle HTTP and translation, use TranslatingLlmClient from the HTTP client crate. The quickstart shows how to keep your own HTTP client. Typed decisions use the separate contract described in Decision Model-Related Protocols.