Xybrid

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:

  1. Load a reasoning model (flagged reasoning: true in its metadata).
  2. run the request as usual — nothing to enable.
  3. Read the answer from text and the chain-of-thought from reasoningContent.

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.

ModelEmits reasoningNotes
lfm2.5-1.2b-thinkingyeshybrid conv+attention reasoning model, ~697 MB
qwen3.5-0.8b / qwen3.5-2bsometimesQwen 3.5 thinking mode
LFM3 1.7B / LFM3-VL 2B bundlesyes, when flaggedits 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.

On this page