Models & requests

The Model trait that abstracts every LLM provider, the LlmRequest and LlmResponse types that travel through it, and the genai_types wire layer underneath.

Every LLM in adk-rs sits behind one trait: Model. Agents build a provider-neutral LlmRequest, the model returns a provider-neutral LlmResponse, and the wire-level details — Gemini JSON, Anthropic Messages, OpenAI chat completions — stay inside the provider crates. Swap models by swapping one Arc<dyn Model>.

The Model trait

Defined in src/core/model.rs, the trait has two required methods and one default. Implementations must be Send + Sync + Debug + 'static, so a model handle can be shared freely across agents and tasks.

fn name(&self) -> &str
Canonical name of this instance, e.g. "gemini-2.5-flash".
fn supported_models(&self) -> &'static [&'static str]
Glob-like name patterns this provider can serve (e.g. "gemini-*"). Used by ModelRegistry to dispatch by model name.
async fn generate_content(&self, req: LlmRequest) -> Result<LlmResponse>
Single-shot generation. The only method a custom model must really implement.
async fn stream_generate_content(&self, req: LlmRequest) -> Result<LlmResponseStream>
Streaming generation. The default implementation calls generate_content and wraps the result in a one-element stream, so non-streaming backends work everywhere streaming is expected.

ModelRegistry

ModelRegistry maps model-name patterns to provider instances. register indexes a model under its exact name() and under every supported_models() glob; get checks exact names first, then walks the glob patterns in insertion order and returns the first match. There is no global state — registries are plain values.

Dispatching by namerust
use adk_rs::core::{Model, ModelRegistry};
use std::sync::Arc;

let mut registry = ModelRegistry::new();
registry.register(Arc::new(gemini));    // supported_models: ["gemini-*"]
registry.register(Arc::new(claude));    // supported_models: ["claude-*"]

let m: Arc<dyn Model> = registry.get("gemini-2.5-pro").expect("glob match");

Anatomy of an LlmRequest

LlmRequest (in src/core/llm_request.rs) bundles everything one model call needs. The system instruction is not in contents — it lives in config.system_instruction.

FieldTypePurpose
modelOption<String>Model identifier, e.g. gemini-2.5-flash.
contentsVec<Content>The conversation history sent to the model.
configGenerateContentConfigSystem instruction, tool declarations, sampling knobs.
tools_dictHashMap<String, Arc<dyn DynTool>>Live tool objects keyed by name, used by the agent to dispatch FunctionCalls. Skipped during serialization.
cache_configOption<ContextCacheConfig>Opt-in context caching: Gemini caches the stable prefix server-side via explicit cachedContents, Anthropic maps it to a prompt-caching cache_control breakpoint, OpenAI-compatible endpoints ignore it.
fn append_system_text(&mut self, text: &str)
Appends to (or sets) config.system_instruction, joining segments with a blank line.
fn append_function_declarations(&mut self, decls: impl IntoIterator<Item = FunctionDeclaration>)
Adds tool declarations to config.tools, merging into an existing Tool::FunctionDeclarations entry when one is present so the wire payload stays a single list.
fn set_output_schema(&mut self, schema: Schema)
Sets config.response_schema and forces config.response_mime_type to application/json — the mechanism behind structured output.

Anatomy of an LlmResponse

LlmResponse is the provider-neutral payload returned by both single-shot calls and each streaming chunk. A response is either content-bearing or error-bearing; is_error() checks whether error_code is set.

FieldTypePurpose
contentOption<Content>Generated content (text, function calls, code, ...).
finish_reasonOption<FinishReason>Why generation stopped: Stop, MaxTokens, Safety, Recitation, MalformedFunctionCall, ImageSafety, UnexpectedToolCall, and friends. Unknown wire values deserialize to a catch-all Unknown variant instead of failing the response.
usage_metadataOption<UsageMetadata>Token counts: prompt_token_count, candidates_token_count, total_token_count, cached_content_token_count, thoughts_token_count.
cache_metadataOption<CacheMetadata>Cache name and hit/miss flag, set by cache-capable providers when a ContextCacheConfig is active.
error_code / error_messageOption<String>Provider-specific error info; populated from non-Stop finish reasons or prompt-feedback block reasons.
grounding_metadata / citation_metadataoptionalSearch-grounding chunks and citations (Gemini built-in tools).
model_version / interrupted / custom_metadataoptionalProducing model, mid-stream interruption flag, free-form metadata.
fn from_generate(resp: GenerateContentResponse) -> Self
Builds an LlmResponse from a wire-level response: takes the first candidate; if it has parts or finished with Stop it becomes content, otherwise the finish reason becomes error_code. Blocked prompts surface prompt_feedback.block_reason as the error.
fn is_error(&self) -> bool
True when error_code is set.
fn function_calls(&self) -> Vec<FunctionCall>
Extracts every Part::FunctionCall from the content.
fn function_responses(&self) -> Vec<FunctionResponse>
Extracts every Part::FunctionResponse from the content.

The genai_types layer

The genai_types module holds the wire-neutral data shapes shared by every provider. A Content is a {role, parts} pair — Role is User, Model, System, or Tool — with helpers Content::user_text, Content::model_text, Content::system_text, and text_concat() to join all text parts.

The Part enum is the unit of content. Gemini discriminates parts by field presence rather than a tag, so Part carries hand-written Serialize/Deserialize impls. The variants:

  • Part::Text(String) — plain text. Build with Part::text("...").
  • Part::InlineData(InlineData) — inline base64 binary with a MIME type. Build from raw bytes with Part::inline_bytes(mime, bytes).
  • Part::FileData(FileData) — external file reference (file_uri + mime_type).
  • Part::FunctionCall(FunctionCall) — a model-emitted tool call. FunctionCall carries a thought_signature: Option<String>: Gemini thinking models attach a thoughtSignature to function-call parts, and on the wire it rides at the part level as a sibling key.
  • Part::FunctionResponse(FunctionResponse) — a tool-emitted result.
  • Part::ExecutableCode(ExecutableCode) — code the model wants executed (language, code).
  • Part::CodeExecutionResult(CodeExecutionResult) — execution outcome (outcome, output). See code execution.
  • Part::Thought(Thought) — a reasoning trace: Thought { text: String, signature: Option<String> }, serialized as {"text": ..., "thought": true} plus a sibling "thoughtSignature" key when signed. Provider signatures (Anthropic thinking signature, Gemini thoughtSignature) must round-trip verbatim — never synthesise one.
  • Part::RedactedThought(String) — encrypted reasoning the provider withheld (Anthropic redacted_thinking); the opaque payload is echoed back verbatim on later turns, serialized as "redactedThought".

GenerateContentConfig knobs

Beyond system_instruction and tools, GenerateContentConfig exposes sampling and safety controls: temperature, top_p, top_k, max_output_tokens, candidate_count, stop_sequences, seed, presence_penalty, frequency_penalty; response_mime_type + response_schema for structured output; safety_settings (a list of SafetySetting { category, threshold }); thinking_config (ThinkingConfig { thinking_budget, include_thoughts }); and tool_config (ToolMode::Auto / Any / None plus allowed_function_names). The default config serializes to an empty JSON object — only what you set goes over the wire.

Implementing a custom Model

A canned-response modelrust
use adk_rs::core::{LlmRequest, LlmResponse, Model};
use adk_rs::genai_types::Content;
use async_trait::async_trait;

#[derive(Debug)]
struct CannedModel;

#[async_trait]
impl Model for CannedModel {
    fn name(&self) -> &str {
        "canned-1"
    }

    fn supported_models(&self) -> &'static [&'static str] {
        &["canned-*"]
    }

    async fn generate_content(&self, _req: LlmRequest) -> adk_rs::Result<LlmResponse> {
        Ok(LlmResponse {
            content: Some(Content::model_text("Always the same answer.")),
            ..LlmResponse::default()
        })
    }
    // stream_generate_content: default impl wraps this in a 1-element stream.
}
  • Providers — the shipped Gemini, Anthropic, and OpenAi implementations.
  • Structured outputset_output_schema end to end.
  • Context cachingcache_config and cache_metadata in detail.
  • Events — how LlmResponse values become session events.