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 |
Reading Reasoning
Same run, same result — reasoning is a sibling of text.
Swift
let model = try XybridModel(fromRegistry: "lfm2.5-1.2b-thinking")
let result = try model.run(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 = XybridModel("lfm2.5-1.2b-thinking")
val result = model.run(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.
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 (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.