Reasoning¶
Independent implementation of the documented System One wire format — not affiliated with TypeSafe.
reasoning=ReasoningConfig(...) selects jevper's native or two-step reasoning path. Whether the provider
receives a reasoning parameter depends on the fields applicable to the selected surface and the model.
effort—none,minimal,low,medium,high,xhigh,maxsummary—auto,concise,detailedcontext—auto,current_turn,all_turnsmode—auto,native,two_stepbudget_tokens— the manual thinking budget for the Messages surface'sthinkingfield; with neither this field nor caller-suppliedextra_body["thinking"], jevper sends nothinkingblock, andeffortis never translated into a budget. Claude 4.6 deprecated manual budgets, and Claude 4.7 and later reject them with400; for 4.6 and newer, Anthropic's supported path isthinking={"type": "adaptive"}(optionally withdisplay: "summarized") andoutput_config={"effort": ...}, while 4.5 and earlier use the manual form. Callers can pass those fields throughextra_body; jevper does not construct them. Where the manual form applies, Anthropic requires at least 1024 and a budget strictly belowmax_tokens. Theinterleaved-thinking-2025-05-14beta on the Messages API with tools permits the budget to exceedmax_tokens, but jevper does not use that exception and refuses locally. When native mode sends aReasoningConfigbudget, jevper grows the Messagesmax_tokensdefault to1024 + budget_tokens; explicitmode="two_step"supplies no configthinkingand leaves jevper's default at1024. A caller-supplied manual thinking budget is sized the same way;extra_body={"max_tokens": n}still wins, butnmust be greater than the sent budget. A server that refuses the budget value — SGLang answersbudget_tokens: must be at least 1024— gets its own error back rather than a silent re-ask without thinking, because a bad number is not a missing field. A field-capability refusal — evidence namingreasoning,thinking, orbudget_tokensbut none of jevper's value markers — drops the field, re-asks, and remembers the limit for that model and surface.
Mode resolution¶
mode |
Responses surface | Chat Completions surface | Messages surface |
|---|---|---|---|
auto (default) |
native |
two_step |
two_step, or native when budget_tokens is set |
native |
answer call; non-null effort/summary/context form reasoning |
answer call; effort becomes reasoning_effort |
answer call; budget becomes thinking |
two_step |
analysis call, then answer call | analysis call, then answer call | analysis call, then answer call |
| reasoning not configured | off | off | off |
response.debug["reasoning_mode"] is the mode for the surface on which the call ended. When questions used
more than one surface, response.debug["reasoning_modes"] maps each question id to the mode derived from
that question's own last attempt.
On the Messages surface a budget resolves auto to native, because a budget is the only reason to ask
for that surface's own thinking. two_step supplies no reasoning config to either call, so it sends no
thinking from ReasoningConfig; a caller-supplied extra_body["thinking"] remains authoritative. On Chat
Completions auto stays two_step — reasoning_effort there is a separate decision, and the surface has
no thinking budget to infer one from.
Native¶
Native uses the answer call rather than a separate analysis call. jevper attaches whichever applicable
reasoning fields are set — none when the config has no field for that surface — and corrective or
capability retries can add provider calls. Readable provider reasoning is normalized into
ReasoningContentPart; Responses and Messages extra fields are retained, nonstandard part types are
canonicalized, and unreadable parts are skipped.
sequenceDiagram
participant C as jevper
participant P as Responses API
C->>P: input, store=false, reasoning={effort,summary}, include=[message.output_text.logprobs, reasoning.encrypted_content]
P-->>C: output_text + reasoning items
Note over C: answer readout from output_text / logprobs
By default, Responses sends the non-null config fields as
reasoning={"effort": ..., "summary": ..., "context": ...}; an extra_body["reasoning"] value replaces
that typed object. Unless overridden in extra_body, Responses sends store=false; unless include is
overridden or the server has refused the include field, jevper adds reasoning.encrypted_content and,
for a logprobs readout, message.output_text.logprobs. Chat Completions sends jevper's
reasoning_effort only when effort is set; an extra_body key wins and can send it when the config
does not, while summary and context have no Chat equivalent. From ReasoningConfig, Messages supplies
thinking only for budget_tokens and only until that field is refused; effort is not translated.
Caller-supplied extra_body["thinking"] is authoritative.
On Claude Fable 5.1, Mythos 5.1, Fable 5, Mythos 5, Mythos Preview, Opus 5.5, Opus 5, Opus 4.8, Opus 4.7,
and Sonnet 5, Anthropic rejects a non-default temperature, top_p, or top_k on every Messages request,
whether thinking is on or off. On older models those restrictions apply only while thinking is on:
temperature and top_k are incompatible, and top_p is allowed only from 0.95 through 1. The
anthropic 1.8 SDK removed all three from messages.create(), so jevper sends its temperature option
through extra_body when it is configured and no thinking budget is sent; caller sampling fields remain
authoritative.
Chat Completions reads reasoning_content, then thinking, then reasoning, then reasoning parts in a
list-shaped message.content; a string becomes a ReasoningTextPart. Responses reads reasoning items
from output, while Messages reads thinking content blocks.
Two-step¶
The base path uses two provider calls per question: an analysis pass, then the answer pass with the analysis replayed as an assistant turn.
sequenceDiagram
participant C as jevper
participant P as provider
C->>P: analysis system prompt, few-shot + question/state turns
P-->>C: free-form considerations
C->>P: answer messages + assistant(trace) + the answer cue
P-->>C: label or JSON answer
Note over C: readout, then finalize
- jevper adds no schema and no logprobs to the analysis call: it is plain text, so the model reasons
freely. Caller-supplied
extra_bodyfields are still forwarded, and few-shot turns appear exactly as in the answer call. - The answer call reuses the full message list, appends
{"role": "assistant", "content": trace}and then the answer cue:{"role": "user", "content": "Now reply with the label only."}forlogprobs/grammar, and"Now reply with the JSON object only."forstructured/discrete, whose answers are JSON. Corrective retries append after that cue. - An analysis pass that returns no text — a reasoning model that thinks without writing output — is handled without sending an empty assistant turn, which several OpenAI-compatible servers reject. If the call did return reasoning, that reasoning text becomes the trace for the answer call; if it returned neither, the answer call runs straight from the question block to the cue.
- For a one-question two-step response with analysis text,
response.reasoning[0].summary[0].textis the synthetic analysis trace. A multi-question response flattens all questions' reasoning in question order, so only the first question's trace occupies index 0.reasoning_text(response.reasoning)returns the trace plus any native summary texts because summaries take precedence over content texts. When the analysis produced no text of its own, no synthetic part is added and the provider's own item is the trace, so the text is not duplicated. - The base two-step path uses two provider calls and sums their reported token counts;
n_callsalso includes capability and corrective retries. Monetary cost depends on the provider, model, generated tokens, and cache reuse.
jevper supplies reasoning config to the analysis call only on the Responses API. On Chat Completions it
sends no typed reasoning_effort on that call — the analysis prompt is the reasoning step — although a
caller-supplied extra_body["reasoning_effort"] is forwarded. If you want the provider's own reasoning on
the Chat surface, use mode="native".
Models that always think¶
Some templates cannot turn thinking off — the Qwen3 2507 Thinking releases are the example: their chat
template primes the assistant turn with thinking and exposes no toggle, so a request that asks for no
reasoning still gets a reasoning span. That costs the label readout. logprobs and grammar read the first
non-whitespace token of the answer, and a model that thinks first and then answers in prose raises
LabelReadoutError carrying whatever it wrote — Okay, on Qwen3-4B-Thinking-2507 served by ollama.
structured and discrete are unaffected: they parse the JSON object out of the answer text, wherever the
reasoning went. Use one of those for an always-thinking model, or a model that can be asked not to think.
A thinking budget the answer has to fit inside¶
A reasoning model spends the output budget before it answers, and the Responses surface reports the
consequence precisely: status: "incomplete" with incomplete_details.reason: "max_output_tokens" and
no output items at all — not a short answer, nothing. jevper reports that as an
IncompleteAnswerError naming the reason, which is the useful diagnosis, because the fix is the
caller's: raise the budget with extra_body={"max_output_tokens": n}.
Measured on opencode Zen, 2026-09-26, muse-spark-1.3-contributor on api="responses":
effort |
256 tokens | 1024 | 4096 |
|---|---|---|---|
| unset | incomplete, no output |
completes | completes |
minimal, low |
completes | completes | completes |
medium, high, xhigh |
incomplete, no output |
completes | completes |
max |
400 |
400 |
400 |
Two things to read off that. jevper's default budget of 1024 is enough for every effort this model
accepts, so nothing has to be raised for it to work — but a smaller budget silently costs you the
whole answer, and more thinking needs more room, not less. And max is in jevper's literal because
some providers accept it; this one refuses it with a 400 that names no field
(param: None, "The request contains invalid parameters"), so there is nothing for the field-downgrade
path to key on and the provider's own 400 is what reaches you. Guessing a substitute effort would be
inventing a mapping the provider did not state, so jevper reports the verdict instead.
Reading the trace¶
from jevper import reasoning_text
reasoning_text(response.reasoning) # joined summary texts, else joined content texts, else ""
ReasoningContentPart keeps unknown fields (id, status, encrypted_content, …) so Responses items can be
replayed into a later request without loss. reasoning_text prefers summaries over content texts, and joins
multiple parts with a blank line.
Responses reasoning items can be dumped and replayed as later Responses input; store=false means the
provider keeps no state for you, and encrypted_content makes the reasoning items portable. Messages
thinking blocks are normalized for reading, so replaying one requires converting it back to Anthropic's
original {"type": "thinking", "thinking": ..., "signature": ...} shape and preserving the block unchanged.