Providers and reasoning
You end up with one agent definition that runs on whichever provider you pick at startup, with the model’s reasoning carried from one turn to the next.
/// Builds the same agent for any chat completion client.fn changelog_agent(llm: &(impl ChatCompletion + Clone + 'static)) -> anyhow::Result<Agent> { let agent = Agent::builder() .llm(llm) .system_prompt("You summarize git history for release notes.") .limit(20) .build()?; Ok(agent)}Source: snippets/src/agents/providers.rs#L14-L22
let mut agent = match std::env::var("LLM_PROVIDER").as_deref() { Ok("anthropic") => changelog_agent( &Anthropic::builder() .default_prompt_model("claude-sonnet-4-5") .build()?, )?, Ok("ollama") => changelog_agent(&Ollama::builder().default_prompt_model("qwen3").build()?)?, Ok("azure") => { let config = AzureConfig::new() .with_api_base("https://my-resource.openai.azure.com") .with_api_version("2024-10-21") .with_deployment_id("gpt-4o-mini") .with_api_key(std::env::var("AZURE_OPENAI_API_KEY")?); let azure: GenericOpenAI<AzureConfig> = GenericOpenAIBuilder::default() .client(async_openai::Client::with_config(config)) .default_prompt_model("gpt-4o-mini") .build()?; changelog_agent(&azure)? } _ => changelog_agent( &OpenAI::builder() .default_prompt_model("gpt-5-mini") .build()?, )?,};
agent .query("Summarize the commits since the last tag.") .await?;Source: snippets/src/agents/providers.rs#L28-L56
AgentBuilder::llm takes a reference to any client that implements ChatCompletion + Clone + 'static. It stores a boxed clone, so Agent has no type parameter for the provider. The tools, hooks and system prompt stay the same whichever client you pass.
The Azure client needs the async-openai crate as a direct dependency, because Swiftide does not re-export its client type. api_base and api_key on the builder only configure the OpenAI client, so Azure passes a full client to client(..):
cargo add async-openai@0.41 --no-default-featuresUse the same version Swiftide depends on, or the client types do not match.
Chat providers
Section titled “Chat providers”Each chat client sits behind its own feature flag. The snippet above needs swiftide-agents and three provider flags:
cargo add swiftide@0.34 --features swiftide-agents,openai,anthropic,ollamaEvery chat client implements ChatCompletion. Providers lists each client with its feature flag, and OpenAI-compatible endpoints covers gateways and base URLs.
The ask-your-codebase showcase builds its client from a base URL, a key and a flag for the Responses API, so the same binary runs against OpenAI or a gateway. It sets them with api_base and api_key:
impl LlmEndpoint { /// Builds a client for this endpoint. /// /// Rate limits and transient errors are retried with exponential backoff, which matters when /// indexing fires many prompts concurrently. After `retry_budget` the error is returned, so /// an unreachable or broken endpoint fails instead of hanging. pub fn client(&self, retry_budget: Duration) -> Result<Arc<dyn SimplePrompt>> { let openai = OpenAI::builder() .api_base(&self.base_url) .api_key(&self.api_key) .default_prompt_model(&self.model) .use_responses_api(matches!(self.api, OpenAiApi::Responses)) .build()?;
let backoff = BackoffConfiguration { max_elapsed_time_sec: retry_budget.as_secs(), ..BackoffConfiguration::default() };
Ok(Arc::new(LanguageModelWithBackOff::new(openai, backoff))) }}Source: examples/ask-your-codebase/src/llm.rs#L79-L100
Bedrock agents
Section titled “Bedrock agents”AwsBedrock talks to the Bedrock Converse API. It reads AWS credentials and the region from your usual Amazon Web Services (AWS) configuration, such as a command-line interface (CLI) profile or environment variables:
let bedrock = AwsBedrock::builder() .default_prompt_model("global.anthropic.claude-sonnet-4-6") .build()?;Source: examples/aws_bedrock_agent.rs#L61-L63
The model ID here is a global inference profile. The agent itself looks like any other:
let mut agent = agents::Agent::builder() .llm(&bedrock) .tools(vec![current_utc_time(), format_timestamp()]) .on_new_message(|_, msg| { let rendered = msg.to_string(); Box::pin(async move { println!("{rendered}"); Ok(()) }) }) .limit(6) .build()?;Source: examples/aws_bedrock_agent.rs#L67-L78
Setup and credentials are on AWS Bedrock.
Reasoning items
Section titled “Reasoning items”On OpenAI, reasoning models return their reasoning only through the Responses API. AwsBedrock returns signed Claude reasoning as ReasoningItem values without extra setup. Reasoning on OpenAI needs the Responses API and a reasoning effort; Responses API and reasoning covers the client options.
// Reasoning models require the Responses API. Enabling reasoning effort also asks for a// summary and encrypted reasoning content (enabled by default). If your OpenAI org is not// verified for reasoning access, summaries may be absent. Disable with// `reasoning_features(false)` if desired.let openai = OpenAI::builder() .default_prompt_model("o3-mini") .default_options(Options::builder().reasoning_effort(ReasoningEffort::Low)) .use_responses_api(true) .build()?;Source: examples/responses_api_reasoning.rs#L34-L42
Each ReasoningItem has a summary list and an optional encrypted_content. This helper joins the summaries:
fn reasoning_summary(reasoning: Option<&[ReasoningItem]>) -> Option<String> { let summary = reasoning .unwrap_or(&[]) .iter() .flat_map(|item| item.summary.iter()) .cloned() .collect::<Vec<_>>() .join("\n");
if summary.is_empty() { None } else { Some(summary) }}Source: examples/responses_api_reasoning.rs#L10-L24
The agent reads them in an after_completion hook:
let mut agent = Agent::builder() .llm(&openai) .on_new_message(|_, message| { if let ChatMessage::Assistant(content, _) = message && let Some(content) = content.as_deref() { println!("Assistant: {content}"); } Box::pin(async move { Ok(()) }) }) .after_completion(|_, response| { if let Some(summary) = reasoning_summary(response.reasoning.as_deref()) { println!("Reasoning summary:\n{summary}"); }
let has_encrypted = response .reasoning .as_ref() .is_some_and(|items| items.iter().any(|item| item.encrypted_content.is_some())); println!("Encrypted reasoning content present: {has_encrypted}"); Box::pin(async move { Ok(()) }) }) .build()?;Source: examples/responses_api_reasoning.rs#L46-L68
After the after_completion hooks run, the agent adds every item in response.reasoning to the message history as a ChatMessage::Reasoning. It adds them before the assistant message, through Agent::add_message, so on_new_message hooks see them too. A hook that empties response.reasoning keeps the items out of the history.
Reasoning replay
Section titled “Reasoning replay”What happens to those items on the next request depends on the client:
- The Responses API client replays them only when
reasoning_effortis set on that client. It then sends back every item with a non-emptyencrypted_contentand setsstoreto false, so the reasoning lives in your message history and survives a saved and restored history (see Context and history). Withoutreasoning_effort, it drops everyChatMessage::Reasoning. AwsBedrockreplays an item only when it has non-emptycontentand a non-emptyencrypted_content, which it sends as the signature. It drops anything else.- The Chat Completions client and
AnthropicleaveChatMessage::Reasoningout of their requests.
Bedrock reasoning effort
Section titled “Bedrock reasoning effort”AwsBedrock has its own Options and its own ReasoningEffort enum in swiftide::integrations::aws_bedrock_v2. Set reasoning_effort in those options and pass them to default_options on the builder. For the Opus 4.5 beta header and the other fields, see AWS Bedrock settings.
Tool schema policies
Section titled “Tool schema policies”ToolSpec::builder().build() checks every schema against Swiftide’s strict contract (see Tools) and rewrites what it can: a nullable type such as ["string", "null"] becomes an anyOf with a null branch, a two-entry oneOf with a null branch becomes an anyOf, annotations next to a $ref are dropped, and every object gets additionalProperties: false.
Each provider then applies its own policy when it builds a request. Only AwsBedrock lets you change it.
OpenAIand the otherGenericOpenAIclients remove$schemaand Rust integer formats such asint32, and list every property inrequired. They reject any remainingoneOfor array-valuedtype, and send the schema withstrict: true.AnthropicandAwsBedrocksend the strict schema unchanged.AwsBedrockmarks tools strict unless you settool_stricttofalsein itsOptions.
Gotchas
Section titled “Gotchas”- The OpenAI schema policy runs when the request is built, not when the tool is built. A schema it rejects fails the first completion with
AgentError::CompletionsFailed, wrapping a permanent error that saystool schema must be OpenAI compatible. - Gateways do not always speak the Responses API. Against the gateway used for the showcase runs, the Responses API fails because the gateway streams events with
"output": null, which the client cannot parse. Use it with OpenAI itself. - OpenAI rejects the
:in Model Context Protocol (MCP) tool names; see Tool names. - Reasoning items are tied to the provider that made them. Bedrock items carry a Bedrock signature in
encrypted_content, and an OpenAI Responses client withreasoning_effortset sends that to OpenAI as encrypted reasoning. OpenAI items usually have nocontent, soAwsBedrockdrops them silently. RemoveChatMessage::Reasoningentries from the message history before you switch providers. AwsBedrock::builder().build()loads the AWS configuration withblock_in_place, so it needs a multi-threaded Tokio runtime. A default#[tokio::test]orflavor = "current_thread"panics. Pass your own client with.client(..)otherwise.- A client’s
on_usagecallback counts every call on that client, including other agents that share it; see Usage callbacks.
Reference
Section titled “Reference”AgentBuilder::llmAgentBuilder::after_completionAgentBuilder::on_new_messageAgent::add_messageChatCompletionGenericOpenAIGenericOpenAIBuilder::use_responses_apiOptions(OpenAI)AzureConfigAnthropicOllamaAwsBedrockOptions(AWS Bedrock)ReasoningEffort(AWS Bedrock)ChatCompletionResponseChatMessageReasoningItemToolSpecAgentError