Reasoning
Read a thinking model's chain-of-thought — on-device, kept out of the answer
Some local models are trained to think before they answer: they emit a chain-of-thought, then the final response. Xybrid keeps that reasoning out of the answer text and surfaces it separately, so you get a clean answer plus an optional window into how the model got there — all on-device.
Xybrid's design rule is that capabilities are data, not entry points. Reasoning rides the existing types with zero new run methods:
- Load a reasoning model (flagged
reasoning: truein its metadata). runthe request as usual — nothing to enable.- Read the answer from
textand the chain-of-thought fromreasoningContent.
The answer in text never contains the <think>…</think> markup, and
reasoningContent is null for models that aren't reasoning models (or that
emitted no reasoning). It's purely additive: existing code that reads text is
unaffected.
Reasoning capture runs on the local llama.cpp backend. Thinking models gate
their <think> block behind a chat-template flag that llama.cpp's legacy
template applier doesn't render, so xybrid primes the channel itself for models
flagged reasoning: true — you don't enable anything, you only choose whether
to read reasoningContent.
Supported Models
Any bundle whose model_metadata.json sets reasoning: true. The reference
model is lfm2.5-1.2b-thinking (Liquid AI's 1.2B reasoning model), which
emits a chain-of-thought before every answer.
| Model | Emits reasoning | Notes |
|---|---|---|
lfm2.5-1.2b-thinking | yes | hybrid conv+attention reasoning model, ~697 MB |
qwen3.5-0.8b / qwen3.5-2b | sometimes | Qwen 3.5 thinking mode |
| LFM3 1.7B / LFM3-VL 2B bundles | yes, when flagged | its chat template switches thinking on or off |
Reading Reasoning
Same run, same result — reasoning is a sibling of text.
Swift
let model = try await Xybrid.model("lfm2.5-1.2b-thinking").load()
let result = try await model.runAsync(envelope: XybridEnvelope.text(
"Is 97 a prime number? Reason, then answer."))
if let answer = result.text { print("Answer:", answer) }
if let reasoning = result.reasoningContent { print("Reasoning:", reasoning) }Kotlin
val model = Xybrid.model("lfm2.5-1.2b-thinking").load()
val result = model.runAsync(Envelope.text("Is 97 a prime number? Reason, then answer."))
result.text?.let { println("Answer: $it") }
result.reasoningContent?.let { println("Reasoning: $it") }Dart (Flutter)
final model = await XybridModelLoader.fromRegistry('lfm2.5-1.2b-thinking').load();
final result = await model.run(XybridEnvelope.text(
'Is 97 a prime number? Reason, then answer.'));
if (result.text != null) print('Answer: ${result.text}');
if (result.reasoningContent != null) print('Reasoning: ${result.reasoningContent}');Rust
use xybrid_sdk::ModelLoader;
use xybrid_sdk::ir::{Envelope, EnvelopeKind};
let model = ModelLoader::from_registry("lfm2.5-1.2b-thinking").load()?;
let result = model.run(
&Envelope::new(EnvelopeKind::Text("Is 97 a prime number? Reason, then answer.".into())),
None,
)?;
println!("Answer: {}", result.text().unwrap_or_default());
if let Some(reasoning) = result.reasoning_content() {
println!("Reasoning: {reasoning}");
}The value is the same in every SDK (String? / Option<&str>, null/None
when absent). The C ABI exposes it as xybrid_result_reasoning_content(), and
the React Native and Unity bindings surface reasoningContent too.
Reasoning in the CLI (xybrid run)
The --show-reasoning flag prints the chain-of-thought in a separate section
under the answer:
xybrid run --model lfm2.5-1.2b-thinking \
--input-text "Is 97 a prime number? Reason, then answer." \
--show-reasoning ── Results ──
…no divisors other than 1 and itself. Thus, 97 is prime.
\boxed{Yes}
── Reasoning ──
Check divisibility by primes ≤ √97 ≈ 9.85: 2, 3, 5, 7. None divide 97…Non-reasoning models print No reasoning emitted instead. xybrid repl also
honors --show-reasoning, printing each turn's reasoning as a dimmed block
above what the turn produced.
Declaring Support in Your Own Bundles
Flag a reasoning model in model_metadata.json so xybrid primes and captures
its chain-of-thought (the /xybrid-init skill can generate metadata):
{
"metadata": {
"reasoning": true
}
}Without the flag, xybrid treats the model as a normal LLM: any <think> blocks
it emits on its own are still stripped from text and captured, but xybrid
won't prime the channel, so a model that only thinks when asked to stays quiet.
Some chat templates switch thinking themselves. LFM3's opens the channel by
default, so xybrid renders LFM3 prompts through the template with the flag:
thinking on with reasoning: true, and the channel pre-filled closed without
it. With thinking off, LFM3 occasionally writes a draft answer, a stray
</think> and then its answer again. The final result keeps the answer after
the tag in text and the draft in reasoningContent, but a streaming
callback has already received the draft and the tag by then.
Recognized formats
Reasoning is captured from these delimiters (in the answer text and, during streaming, suppressed from the live callback):
<think>…</think>— Qwen 3 / DeepSeek-R1 / LFM2.5-thinking / LFM3 (the form xybrid primes).<thinking>…</thinking>— Anthropic-style.<|channel>thought … <channel|>— gemma-4's thought channel. Recognized, but xybrid runs gemma-4 with thinking disabled today, so this path isn't yet exercised end-to-end.
Other bespoke reasoning delimiters aren't parsed.
What Isn't Reasoning
- Whitespace-only blocks — a model that opens and immediately closes a block
with nothing inside produces no
reasoningContent(null), not an empty string. - Streaming — reasoning lands on the final (non-streaming) result; during streaming it's suppressed from the token callback so it never leaks into the live answer.