Skip to content

Stream Ollama reasoning progressively in streamed responses - #374

Closed
foxterm wants to merge 17 commits into
huggingface:mainfrom
foxterm:main
Closed

foxterm wants to merge 17 commits into
huggingface:mainfrom
foxterm:main

Conversation

@foxterm

@foxterm foxterm commented Oct 28, 2026

Copy link
Copy Markdown

Summary

OllamaLanguageModel.streamResponse buffered the model's thinking chunks in a local
variable and only appended a .reasoning transcript entry after the chunk stream
ended. Snapshots were also only yielded for content / usage chunks, so nothing was
emitted during the reasoning phase at all.

Consumers that render snapshot.transcriptEntries (e.g. chat UIs showing the thinking
block) therefore saw the answer stream in first, and the full reasoning text appear all
at once in the final snapshot — reasoning always arrived last, out of order.

Changes

  • Stream reasoning as it arrives: each thinking piece is accumulated into a single
    cumulative .reasoning entry per round, replaced as new pieces come in. This keeps the
    transcript compact and preserves the original thinking text when replaying to Ollama.
  • Yield snapshots for thinking chunks too, so reasoning streams progressively,
    before the answer.
  • Remove the now-redundant end-of-stream reasoning append (both the no-tool-call
    path and the tool-round path).

Behavior

  • The final Response is unchanged: reasoning still lands in transcriptEntries before
    the response / tool calls, and collect() reaches the same end state.
  • toOllamaMessages() replay is unchanged: one cumulative entry means the thinking text
    sent back to the model is exactly as before.

Testing

Built a downstream Swift package that depends on AnyLanguageModel via SPM
(Debug, macOS) — compiles cleanly.

@mattt mattt left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks, @foxterm. Streaming the reasoning as it arrives makes sense, and it should come in before the answer. A few things before we can merge this:

  1. swift format lint fails in CI on OllamaLanguageModel.swift (a missing line break at line 324, and line 332 is too long). Running swift format --in-place on the file should fix both.
  2. Since reasoning now goes into state.entries before toolRounds.record(completed:), a ToolRoundLimitExceeded error would include the reasoning from the round that was rejected, which the error's documentation says it leaves out. I'd expect OllamaReasoningReplayTests to catch this once CI gets past lint. AnthropicLanguageModel handles this by saving state.entries at the start of each round and passing that copy to record.
  3. .reasoning(.ollama(thinking)) creates a new entry, with new IDs, for each chunk. Transcript entries should keep the same ID as they update, so that UIs can track them. Could you update the existing entry's text in place instead, and add a test with a few thinking chunks that checks for one entry with the same ID across snapshots? AnthropicReasoningTests has a similar check.

@mattt

mattt commented Oct 29, 2026

Copy link
Copy Markdown
Collaborator

Thanks for the updates, @foxterm. This PR now does a few different things, so let's split it up to make each part easier to review.

Could you bring this PR back to streaming Ollama reasoning? Two things from my earlier review are still open there: reasoning goes into state.entries before toolRounds.record(completed:), and .ollama(thinking) builds an entry with new IDs on each chunk, so the entry should keep the same ID as its text grows (with a test that checks it).

The tool guidance message is something we can't take in the library. It changes what every Ollama request with tools sends, in one language, and apps can already add that kind of guidance in their own instructions.

The llama.cpp thinking spans and the context size changes would be good as a separate PR. Those change defaults and add public API, so it'd help to have the reasoning for them in the PR description. What do you think?

- Add LlamaPerformanceSnapshot with model size, parameter count, KV cache
  estimate, process footprint, available memory, tokens/s and GPU offload
  layers via llama_model_size/n_params/n_ctx/n_layer/n_embd
- Measure process CPU via proc_pidinfo: host_cpu_load_info idle ticks never
  advance on Apple Silicon so it always reported 100%
- Tighten runtime memory guard to max(estimate/4, 1GB) to prevent system
  memory compression stalls
- Record tokens-per-second per generation under statsLock
@foxterm foxterm closed this Oct 30, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants