Thinking & Reasoning Models
Reasoning models (a.k.a. “thinking” models) produce private chain-of-thought
before their answer: Claude extended thinking (native Anthropic and Bedrock),
OpenAI’s o-series, Gemini thinking, and Ollama models like qwen3 and
deepseek-r1. Kova gives you one consistent way to enable, read, and
account for that reasoning, regardless of provider.
The core rule
Section titled “The core rule”Chain-of-thought is surfaced separately and, as visible reasoning, never re-sent to the model as prompt. You get to see it (for display or logging), but it doesn’t pollute the context or cost you tokens on the next turn.
Where it shows up:
- Blocking calls (
run) →AgentResponse::thinking: Option<String>(from the underlyingModelResponse::thinking). - Streaming (
run_stream) →AgentEvent::ThinkingDelta { text }.
Enabling reasoning, per provider
Section titled “Enabling reasoning, per provider”Each provider exposes the knob its API uses; the output is unified.
Anthropic (native) — adaptive extended thinking is on by default; tune effort:
use kova_sdk::provider::anthropic::AnthropicProviderConfig;
let config = AnthropicProviderConfig::new("claude-opus-4-8") .with_api_key("sk-ant-…") .with_effort("high") // low | medium | high | xhigh | max .with_adaptive_thinking(true); // default onOpenAI o-series — reasoning effort:
use kova_sdk::provider::openai::OpenAiProviderConfig;
let config = OpenAiProviderConfig::new("https://api.openai.com", "o3") .with_api_key("sk-…") .with_reasoning_effort("high"); // "low" | "medium" | "high"AWS Bedrock (Claude extended thinking) — pass a token budget through:
use kova_sdk::provider::bedrock::BedrockProviderConfig;
let config = BedrockProviderConfig::new("us-east-1", "anthropic.claude-sonnet-4-20250514-v1:0") .with_additional_model_request_fields(serde_json::json!({ "budgetTokens": 5000 }));Google Gemini — thinking budget:
use kova_sdk::provider::gemini::GeminiProviderConfig;
// -1 = dynamic/unlimited, 0 = off (default), positive = hard caplet config = GeminiProviderConfig::new("gemini-2.5-flash") .with_api_key("AIza…") .with_thinking_budget(5000);Ollama — think mode (needs a thinking-capable model):
use kova_sdk::provider::ollama::{OllamaProviderConfig, OllamaThink};
let config = OllamaProviderConfig::new("qwen3") .with_think(OllamaThink::High); // Enabled | High | Medium | LowReading the reasoning
Section titled “Reading the reasoning”Blocking
Section titled “Blocking”let history = vec![user_message("Prove that √2 is irrational.")];let response = agent.run(&history).await?;
println!("Answer:\n{}", response.text);if let Some(reasoning) = &response.thinking { eprintln!("\n[model reasoning]\n{reasoning}");}Streaming — separate the two channels
Section titled “Streaming — separate the two channels”Render the answer on stdout and the (dimmed) reasoning on stderr so a user can watch the model think:
use futures::StreamExt;
let stream = agent.run_stream(&history);futures::pin_mut!(stream);while let Some(event) = stream.next().await { match event? { AgentEvent::ThinkingDelta { text } => eprint!("\x1b[2m{text}\x1b[0m"), // dim AgentEvent::TextDelta { text } => print!("{text}"), // answer AgentEvent::TurnCompleted { .. } => {} _ => {} }}Token accounting
Section titled “Token accounting”Thinking tokens are counted where the provider reports them separately:
UsageStats::thinking_tokens: Option<u32>— reasoning tokens as a subset ofoutput_tokens(not additive).Nonemeans the provider doesn’t report a separate count — surfaced honestly as “unknown” rather than a misleading0.- Aggregated across every provider call in a turn:
None + NonestaysNone; any reported value flips the running total toSome.
Which providers report them:
| Provider | Separate thinking-token count? |
|---|---|
| OpenAI (o-series) | ✅ via reasoning_tokens |
| Gemini | ✅ via thoughtsTokenCount |
| Anthropic (native) | ❌ folded into output_tokens (None) |
| Bedrock | ❌ folded into output_tokens (None) |
| Ollama | ❌ folded into output_tokens (None) |
let response = agent.run(&history).await?;let u = &response.usage;match u.thinking_tokens { Some(t) => println!("{t} of {} output tokens were reasoning", u.output_tokens), None => println!("provider doesn't break out reasoning tokens"),}Practical notes
Section titled “Practical notes”- Latency & cost. Higher reasoning effort / larger budgets mean more tokens
and slower responses. Tune
reasoning_effort/budgetTokens/thinking_budgetto the difficulty of the task. - Non-reasoning models ignore the knobs. Setting
reasoning_efforton a non-o-series model, orthinking_budgeton a non-thinking Gemini model, is a no-op — safe to leave in provider-agnostic code. - History stays clean. Because thinking is never persisted, multi-turn conversations don’t accumulate reasoning text, keeping context small.
Key takeaways
Section titled “Key takeaways”- Reasoning is surfaced via
thinking/ThinkingDeltaand never stored or re-sent. - Enable it with the provider’s own knob (
reasoning_effort,budgetTokens,thinking_budget,OllamaThink); the output shape is unified. UsageStats::thinking_tokensis anOptionsubset of output tokens —None= provider doesn’t report it (OpenAI/Gemini do, Bedrock/Ollama don’t).