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 byModelRegistryto 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_contentand 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.
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.
| Field | Type | Purpose |
|---|---|---|
model | Option<String> | Model identifier, e.g. gemini-2.5-flash. |
contents | Vec<Content> | The conversation history sent to the model. |
config | GenerateContentConfig | System instruction, tool declarations, sampling knobs. |
tools_dict | HashMap<String, Arc<dyn DynTool>> | Live tool objects keyed by name, used by the agent to dispatch FunctionCalls. Skipped during serialization. |
cache_config | Option<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 existingTool::FunctionDeclarationsentry when one is present so the wire payload stays a single list. fn set_output_schema(&mut self, schema: Schema)- Sets
config.response_schemaand forcesconfig.response_mime_typetoapplication/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.
| Field | Type | Purpose |
|---|---|---|
content | Option<Content> | Generated content (text, function calls, code, ...). |
finish_reason | Option<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_metadata | Option<UsageMetadata> | Token counts: prompt_token_count, candidates_token_count, total_token_count, cached_content_token_count, thoughts_token_count. |
cache_metadata | Option<CacheMetadata> | Cache name and hit/miss flag, set by cache-capable providers when a ContextCacheConfig is active. |
error_code / error_message | Option<String> | Provider-specific error info; populated from non-Stop finish reasons or prompt-feedback block reasons. |
grounding_metadata / citation_metadata | optional | Search-grounding chunks and citations (Gemini built-in tools). |
model_version / interrupted / custom_metadata | optional | Producing model, mid-stream interruption flag, free-form metadata. |
fn from_generate(resp: GenerateContentResponse) -> Self- Builds an
LlmResponsefrom a wire-level response: takes the first candidate; if it has parts or finished withStopit becomes content, otherwise the finish reason becomeserror_code. Blocked prompts surfaceprompt_feedback.block_reasonas the error. fn is_error(&self) -> bool- True when
error_codeis set. fn function_calls(&self) -> Vec<FunctionCall>- Extracts every
Part::FunctionCallfrom the content. fn function_responses(&self) -> Vec<FunctionResponse>- Extracts every
Part::FunctionResponsefrom 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 withPart::text("...").Part::InlineData(InlineData)— inline base64 binary with a MIME type. Build from raw bytes withPart::inline_bytes(mime, bytes).Part::FileData(FileData)— external file reference (file_uri+mime_type).Part::FunctionCall(FunctionCall)— a model-emitted tool call.FunctionCallcarries athought_signature: Option<String>: Gemini thinking models attach athoughtSignatureto 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, GeminithoughtSignature) must round-trip verbatim — never synthesise one.Part::RedactedThought(String)— encrypted reasoning the provider withheld (Anthropicredacted_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
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.
}Related pages
- Providers — the shipped
Gemini,Anthropic, andOpenAiimplementations. - Structured output —
set_output_schemaend to end. - Context caching —
cache_configandcache_metadatain detail. - Events — how
LlmResponsevalues become session events.