Adapters translate between the canonical ModelInput / LlmEvent algebra and provider-specific
wire formats. Lutum ships three adapters: ClaudeAdapter (Anthropic Messages API),
OpenAiAdapter (OpenAI Responses API and OpenAI-compatible backends), and
OpenRouterGenerationClient (OpenRouter usage recovery).
On This Page
ClaudeAdapter
ClaudeAdapter targets the Anthropic Claude Messages API. Enable it with the claude feature:
[dependencies]
lutum = { version = "0.1.0", features = ["claude"] }
lutum-claude = "0.1.0"use std::sync::Arc;
use lutum::{Lutum, ModelName, SharedPoolBudgetManager, SharedPoolBudgetOptions};
use lutum_claude::ClaudeAdapter;
let adapter = ClaudeAdapter::new(std::env::var("ANTHROPIC_API_KEY")?)
.with_default_model(ModelName::new("claude-opus-4-5")?);
let llm = Lutum::new(
Arc::new(adapter),
SharedPoolBudgetManager::new(SharedPoolBudgetOptions::default()),
);Extended Thinking
Claude supports extended thinking. The simplest way is to set a default budget on the adapter:
use lutum_claude::ClaudeAdapter;
let adapter = ClaudeAdapter::new(api_key)
.with_default_model(ModelName::new("claude-opus-4-5")?)
.with_default_thinking_budget(8000); // budget_tokens applied to every requestFor per-request control, implement ResolveBudgetTokens and register it with
with_resolve_budget_tokens. The hook receives the request's RequestExtensions and the
adapter's default budget, and returns the budget to use (or None to disable thinking):
use lutum_claude::{ClaudeAdapter, ResolveBudgetTokens};
// User-defined extension type — not provided by lutum_claude
#[(Clone)]
struct ThinkingBudget(u32);
#[::(ResolveBudgetTokens)]
async fn my_budget_resolver(
extensions: &RequestExtensions,
default: Option<u32>,
) -> Option<u32> {
extensions.get::<ThinkingBudget>().map(|b| b.0).or(default)
}
let adapter = ClaudeAdapter::new(api_key)
.with_resolve_budget_tokens(MyBudgetResolver);
// Attach a per-request budget via RequestExtensions
let result = session
.text_turn(&llm)
.ext(ThinkingBudget(8000))
.collect()
.await?;OpenAiAdapter
OpenAiAdapter targets the OpenAI Responses API. It also works with any OpenAI-compatible
endpoint (Ollama, Azure OpenAI, Together AI, etc.):
[dependencies]
lutum = { version = "0.1.0", features = ["openai"] }
lutum-openai = "0.1.0"use std::sync::Arc;
use lutum::{Lutum, ModelName, SharedPoolBudgetManager, SharedPoolBudgetOptions};
use lutum_openai::OpenAiAdapter;
// Standard OpenAI
let adapter = OpenAiAdapter::new(std::env::var("OPENAI_API_KEY")?)
.with_default_model(ModelName::new("gpt-4o")?);
// Ollama (OpenAI-compatible)
let adapter = OpenAiAdapter::new("ollama")
.with_base_url("http://localhost:11434/v1/")
.with_default_model(ModelName::new("llama3.2")?);
let llm = Lutum::new(
Arc::new(adapter),
SharedPoolBudgetManager::new(SharedPoolBudgetOptions::default()),
);Reasoning Effort
OpenAI o-series models support a reasoning_effort parameter. Configure it via
ReasoningEffortResolver in OpenAiHooksSet:
use lutum_openai::{OpenAiAdapter, OpenAiHooksSet, ReasoningEffort};
#[::(ResolveReasoningEffort)]
async fn my_resolver(
extensions: &RequestExtensions,
last: Option<Option<ReasoningEffort>>,
) -> Option<ReasoningEffort> {
Some(ReasoningEffort::High)
}
let hooks = OpenAiHooksSet::new().with_reasoning_effort(MyResolver);
let adapter = OpenAiAdapter::new(api_key).with_hooks(hooks);OpenRouter
OpenRouter returns usage data out-of-band via GET /api/v1/generation. Use
OpenRouterGenerationClient as the UsageRecoveryAdapter and wire it in via Lutum::from_parts:
[dependencies]
lutum-openrouter = "0.1.0"use std::sync::Arc;
use lutum::{Lutum, ModelName, SharedPoolBudgetManager, SharedPoolBudgetOptions};
use lutum_openai::OpenAiAdapter;
use lutum_openrouter::OpenRouterGenerationClient;
let turn_adapter = Arc::new(
OpenAiAdapter::new(std::env::var("OPENROUTER_API_KEY")?)
.with_base_url("https://openrouter.ai/api/v1/")
.with_default_model(ModelName::new("anthropic/claude-opus-4-5")?)
);
let recovery = Arc::new(
OpenRouterGenerationClient::new(std::env::var("OPENROUTER_API_KEY")?)
);
let llm = Lutum::from_parts(
turn_adapter.clone(),
turn_adapter, // completion adapter (same for OpenRouter)
recovery, // usage recovery adapter
SharedPoolBudgetManager::new(SharedPoolBudgetOptions::default()),
);Dynamic Model Routing
Both ClaudeAdapter and OpenAiAdapter accept a ModelSelector for request-level model
routing. Implement ModelSelector::select_model(&RequestExtensions) -> ModelSelection and attach
it to the adapter:
use lutum_openai::{OpenAiAdapter, ModelSelection, SelectOpenaiModel};
struct TenantModelRouter;
impl SelectOpenaiModel for TenantModelRouter {
fn select_model(&self, extensions: &RequestExtensions) -> ModelSelection {
let model = if extensions.get::<PremiumTenant>().is_some() {
"gpt-4o"
} else {
"gpt-4o-mini"
};
ModelSelection { primary: Some(model.into()), fallbacks: None }
}
}
let adapter = OpenAiAdapter::new(api_key)
.with_model_selector(TenantModelRouter);Mixing Adapters with from_parts
Lutum::from_parts accepts the three adapter traits separately, allowing any combination:
// Claude turns, OpenAI completion, OpenRouter usage recovery
let llm = Lutum::from_parts(
Arc::new(claude_adapter), // dyn TurnAdapter
Arc::new(openai_adapter), // dyn CompletionAdapter
Arc::new(openrouter), // dyn UsageRecoveryAdapter
budget,
);This is also required when you want to use the structured_completion or completion APIs,
since Lutum::new does not wire a CompletionAdapter.