The genai_client exposes a single, provider-agnostic way to control model
reasoning ("thinking") across every supported provider. Callers send two keys in
the model param map and the client normalizes them into the correct native shape
for whichever provider the engine points to:
thinking— turn reasoning on/off. Accepts a bool, the strings"true"/"false", or an Anthropic/Claude-Code passthrough dict ({"type": "enabled"|"adaptive"|"disabled", "budget_tokens": N}).effort— how hard to think. One of the canonical levelsnone | minimal | low | medium | high | xhigh | max, or an integer token budget (which gets bucketed to a level).
The legacy key thinking_budget (int) is still honored for backwards
compatibility.
The providers split into two camps. Effort-based: OpenAI, modern Anthropic (Opus 4.6+/Sonnet 4.6/Fable), Gemini 3.x. Token-budget-based: Gemini 2.5, Bedrock Converse, legacy Anthropic. A shared canonical
effort ⇄ budgetladder lets one set of inputs work everywhere.
The keys live in the model param map that reaches the message builders:
RunAgent(...)pixel — pass them underparamValues:paramValues=[{"thinking":true, "effort":"high"}]. (Put them underparamValues, the model param map — notagentParams, which is for harness hooks.)LLM(...)pixel — sameparamValues=[{...}]form.- Python client (
gaas_gpt_model) —model.ask(..., param_dict={"thinking": True, "effort": "high"})(the wrapper serializesparam_dicttoparamValues). - SMSS —
ModelSettings.thinking,ModelSettings.effort, andModelSettings.thinking_budgetact as fallbacks when the param map omits them.
normalize_reasoning(param_map, model_settings) collapses the inputs into a
canonical ResolvedReasoning(enabled, effort, budget) (or None when reasoning
is off) and pops thinking / effort / thinking_budget from the param map
so they never leak into the request body. Each provider's message builder then
maps that result onto its native shape:
| Builder | Method | Native shape |
|---|---|---|
openai/openai_message_builder.py |
_resolve_extended_reasoning (Responses), _resolve_reasoning_effort (Chat Completions) |
reasoning={"effort": ..., "summary": "auto"} / reasoning_effort=... |
anthropic/anthropic_message_builder.py |
_resolve_extended_thinking |
modern: thinking={"type":"adaptive"} + output_config={"effort": ...}; legacy: thinking={"type":"enabled","budget_tokens": N} |
google_genai/google_genai_builder.py |
_resolve_thinking_config |
Gemini 3: ThinkingConfig(thinking_level=...); Gemini 2.5: ThinkingConfig(thinking_budget=N) |
bedrock/bedrock_message_builder.py |
_resolve_reasoning_config / _apply_reasoning |
additionalModelRequestFields.reasoning_config={"type":"enabled","budget_tokens": N} |
You always send one of the 7 canonical values; each builder maps it to that
provider's native value (budgets have a floor of 1024):
You send effort |
OpenAI reasoning.effort |
Claude modern output_config.effort |
Claude legacy budget_tokens |
Gemini 3 thinking_level |
Gemini 2.5 thinking_budget |
Bedrock reasoning_config.budget_tokens |
|---|---|---|---|---|---|---|
none |
none |
low |
1024 | MINIMAL |
1024 | 1024 |
minimal |
minimal |
low |
1024 | MINIMAL |
1024 | 1024 |
low |
low |
low |
4096 | LOW |
4096 | 4096 |
medium |
medium |
medium |
8192 | MEDIUM |
8192 | 8192 |
high |
high |
high |
24576 | HIGH |
24576 | 24576 |
xhigh |
xhigh |
xhigh (→ high unless Opus 4.7+/Fable) |
32768 | HIGH |
32768 | 32768 |
max |
high |
max |
63999 | HIGH |
63999 | 63999 |
This is why the mapping clamps — the native domains differ:
| Provider | Native effort domain |
|---|---|
| OpenAI | none, minimal, low, medium, high, xhigh — but per-model (gpt-5: minimal/low/medium/high; gpt-5.1: none/low/medium/high; gpt-5.5: +xhigh). No max. |
| Claude modern (Opus 4.6+/Sonnet 4.6/Fable) | low, medium, high, xhigh, max (xhigh only Fable/Opus 4.7+) |
| Claude legacy / Bedrock / Gemini 2.5 | token budget (no effort) — effort is bucketed to a budget |
| Gemini 3 | MINIMAL, LOW, MEDIUM, HIGH (no xhigh/max) |
An unrecognized effort (typo, "ultra", "turbo", etc.) never raises.
_coerce_effort returns None, then normalize_reasoning falls back in order:
- SMSS
ModelSettings.effort(if configured) - an explicit
thinking_budget(bucketed to a level) DEFAULT_EFFORT = "medium"— the final fallback
So {"thinking": true, "effort": "ultra"} → medium. But
{"thinking": true, "effort": "ultra", "thinking_budget": 20000} → high (the
budget wins over the unusable string before the default applies).
- Two clamp layers — only the first defaults to
medium. An invalid value →medium(above). A valid canonical value the target model doesn't support → the builder clamps it to the nearest supported value deterministically (e.g.max→highon OpenAI,none→lowon Claude,xhigh→HIGHon Gemini 3), notmedium. - OpenAI is the one place a valid value can still 400. The client does not
carry OpenAI's per-model matrix, so
xhigh/none/minimalpass through as-is. Sendingxhighto a model that doesn't accept it (e.g. gpt-5.1) is rejected by OpenAI, not the client.low/medium/highare safe on every OpenAI reasoning model. Claude, Gemini, and Bedrock always self-clamp and won't error on effort. effort: "none"is not the same asthinking: false. Withthinking: true, effort: "none"you still get a minimal-but-on request (budget floored to1024/MINIMAL/none). To fully disable reasoning, sendthinking: false(or omitthinking).- Budgets are approximate; providers enforce their own ranges. We do not
clamp per-model budget ceilings (e.g. Gemini 2.5 Pro caps
thinking_budgetat 32768), somax/xhighon a small-ceiling model may be clamped or rejected by that provider.
Bare boolean — reasoning on at the default medium on every provider:
RunAgent(
roomId=["my-room-123"],
engine=["<model-engine-id>"],
command=["<encode>Find the failing test and propose a fix</encode>"],
paramValues=[{"thinking":true}]
);
Explicit effort:
paramValues=[{"thinking":true, "effort":"high"}]
Legacy token budget (bucketed for effort-based providers, used directly for budget-based ones):
paramValues=[{"thinking":true, "thinking_budget":12000}]
Turn reasoning off:
paramValues=[{"thinking":false}]
Python client:
model.ask(command="Summarize this incident", param_dict={"thinking": True, "effort": "high"})