Blog

Luis Majano

September 15, 2026

Spread the word


Share your thoughts

Reasoning-capable models have been usable in BoxLang AI for a while. params passes straight through to the provider body, so params: { thinking: { type: "enabled", budget_tokens: 10000 } } for Claude, or params: { reasoning_effort: "high" } for OpenAI, already reached the API. What never worked was reading the reasoning back. It was parsed out on arrival and silently dropped. In 3.4.0, that's fixed, and it's fixed the same way for every provider.

One key, every provider

Reasoning now surfaces on the same OpenAI-shaped envelope every provider already normalizes onto: choices[].message.reasoning synchronously, choices[].delta.reasoning while streaming.

// Enable extended thinking on Claude
result = aiChat( "Solve this step by step: ...", params: {
    thinking: { type: "enabled", budget_tokens: 10000 }
}, options: { returnFormat: "raw" } )

reasoning = result.choices[1].message.reasoning ?: ""  // "" when the model didn't think, absence is normal
answer    = result.choices[1].message.content
// OpenAI reasoning effort
result = aiChat( "...", params: { reasoning_effort: "high" }, provider: "openai" )

Streaming works the same way, reasoning arrives on delta.reasoning, ahead of delta.content:

aiChatStream( "...", ( chunk ) => {
    if ( !isNull( chunk.choices?.first()?.delta?.reasoning ) ) {
        print( chunk.choices.first().delta.reasoning )  // thinking, as it streams
    }
} )

Why this needed real work per provider

Every provider spells reasoning differently on the wire. Anthropic sends thinking_delta. DeepSeek sends reasoning_content. Ollama nests it under message.thinking. Claude-on-Bedrock uses delta.thinking. Every one of those is now either inherited automatically from BaseService's normalization or mapped explicitly, so your code reads chunk.choices.first().delta.reasoning regardless of which model is actually behind the call. MockService can even script reasoning output for offline tests.

Absence is normal, not an error

A model or provider that doesn't reason simply omits the key. Always read it defensively:

reasoning = delta.reasoning ?: ""

Reasoning is deliberately not a declared capability the way chat or embeddings are, because it varies per model, not per provider. Sonnet reasons, Haiku doesn't. GPT-5 reasons, GPT-4o doesn't. There's no single flag on a provider that tells you.

Kept strictly out of memory

Reasoning is kept separate from content and is never persisted to agent memory. That's a deliberate design choice, not an oversight: replaying a model's private thinking back to it on the next turn, as if it had actually said that out loud, changes its behavior in ways you didn't ask for.

One thing to watch on OpenAI: tools and reasoning don't mix (yet)

OpenAI doesn't accept function tools alongside active reasoning on /v1/chat/completions, the endpoint this module speaks. This bites even if you never explicitly set reasoning_effort, because OpenAI's default model reasons by default:

Function tools with reasoning_effort are not supported for <model> in /v1/chat/completions.
To use function tools, use /v1/responses or set reasoning_effort to 'none'.

Until Responses API support lands, pick one of two escapes:

// Tools, no reasoning, name a non-reasoning model explicitly
aiChat( "How hot is it in KC?", params: { tools: [ tool ], model: "gpt-4o" } )

// Tools on a reasoning model, turn reasoning off for the call
aiChat( "How hot is it in KC?", params: { tools: [ tool ], reasoning_effort: "none" } )

Claude, for one, isn't affected, it accepts extended thinking and tools together on its single endpoint.

Why it matters

Reasoning traces are one of the most useful debugging tools an agent gives you, and until now BoxLang AI threw them away. Now you can log them, show them to a human in a HITL approval prompt, or just use them to understand why an agent made the call it made, without writing a single provider-specific branch to get there.

That closes out the series. Five features, one theme: agents you can trust, because a human can step in when it matters, and because what the model reads and writes is defended on both sides.

Docs: Reasoning

Add Your Comment

Recent Entries

BoxLang 1.17 Series Part 4 : WriteDump Enhanced!

BoxLang 1.17 Series Part 4 : WriteDump Enhanced!

You call writeDump() on an ORM entity or a very rich class graph . The browser locks up. Thirty seconds later you get a page with forty thousand rows on it, you scroll for a while, you give up, and you go edit your code to dump a sub-key instead. Or worse, you crash the server.

Luis Majano
Luis Majano
September 11, 2026
BoxLang AI 3.4 Blog Series Part 4 : Locking Down Prompt Injection

BoxLang AI 3.4 Blog Series Part 4 : Locking Down Prompt Injection

LLM applications face a class of attack traditional input validation was never built for: prompt injection. An attacker embeds instructions in user input, a retrieved document, a web page your tool fetched, or an MCP result, trying to override your system prompt, exfiltrate data, or hijack a tool call. BoxLang AI 3.4.0 ships four layered, configurable defenses against exactly this, plus one more that's on unconditionally.

Luis Majano
Luis Majano
September 11, 2026