reasoning: reasoning text missing entirely despite reasoning tokens (thinking never shown, no text leak) · Issue #54886 · anomalyco/opencode · GitHub
Skip to content

reasoning: reasoning text missing entirely despite reasoning tokens (thinking never shown, no text leak) #54886

Open

Description

@hollychan05-ctrl

reasoning: reasoning text missing entirely despite reasoning tokens (thinking never shown, no text leak)

Summary

For a small but session-clustered set of assistant messages, opencode stores
tokens.reasoning > 0 but the message contains no reasoning part at all —
and the stored text is clean (no inline chain-of-thought, no </think> tag).
Because there is no reasoning part, the TUI never shows a thinking block for
those turns, even though the model did think.

This is not the same as the inline-</think> leak (see Related): here the
reasoning content is absent from the message entirely rather than leaked into
text.

Environment

  • opencode version: 2.0.27
  • OS: Windows (10.0.26200, x64)
  • Terminal: xterm-256color
  • Shell: cmd.exe
  • Install/channel: release
  • Active plugins: @firecrawl/opencode, opencode-tavily
  • Model: opencode/big-pickle (variant: default), agent build

Reproduction

Observed from the local session database (normally created sessions — not
imported/migrated).

  1. Select opencode/big-pickle.
  2. Send a handful of ordinary message turns.
  3. In some sessions, every completed turn stores tokens.reasoning in the
    ~1090–1340 range, but the assistant message content contains only a single
    text part.
  4. The TUI shows no thinking block for any of those turns.

Expected Behavior

When the provider reports reasoning tokens and delivers reasoning content, a
reasoning part should be persisted on the message (and rendered as a
collapsible thinking block) — the same as when reasoning_content is returned.

Actual Behavior

The message has no reasoning part, and the text part does not contain the
chain-of-thought either. The reasoning content is simply not present anywhere in
the stored message.

Aggregate scan of the local session DB (all opencode/big-pickle assistant
messages):

bucket count
tokens.reasoning > 0 2,754
— of those, contain a reasoning part 2,693
— of those, no reasoning part 61
— — of those 61, text contains a </think>/<think marker 0

The 61 affected messages are clustered by session (several affected turns
per session). One session had 6/6 completed turns affected, each with
tokens.reasoning between 1,090 and 1,337 and content = ["text"] only.

Anonymized structure of one affected message (text content omitted):

{
  "agent": "build",
  "model": { "id": "big-pickle", "providerID": "opencode", "variant": "default" },
  "content": [ { "type": "text", "text": "<omitted>" } ],
  "finish": "stop",
  "rawFinish": "stop",
  "tokens": { "input": 1207, "output": 1241, "reasoning": 1090,
              "cache": { "read": 12365, "write": 0 } }
}

For comparison, a normal message that does persist reasoning looks like:

"content": [
  { "type": "reasoning", "text": "<omitted>", "state": "...", "time": { } },
  { "type": "text", "text": "<omitted>" }
]

Related Issues

Additional Context

  • The affected messages are structurally identical to normal ones (same
    finish, rawFinish, agent, model keys), so the trigger is not visible
    at the message level.
  • Responses in affected sessions appear effectively non-streamed
    (time.streamed ≈ time.completed), but that is also true of most messages
    that do persist a reasoning part, so streaming alone does not explain it.
  • Workaround: start a new session — the large majority of sessions persist
    reasoning correctly.

Activity

  1. opencode-agent commented on Oct 12, 2026

    @opencode-agent
    Contributor

    Thanks for the detailed report and the database scan. I was able to reproduce this on 2.0.27 with opencode/big-pickle.

    I ran a series of one-shot turns (opencode run -m opencode/big-pickle "Write a short haiku about number N, thinking carefully first."), then scanned the local session database. On a fresh database, 4 of 24 assistant messages with tokens.reasoning > 0 had only a text part. Their reasoning token counts were 1174, 461, 380 and 61. Their text was clean, with no inline chain-of-thought or <think> tags. The other turns had a normal reasoning part. The script below runs the turns and the scan.

    What I couldn't establish is where the reasoning gets lost. In the one turn I managed to capture on the wire, Zen streamed reasoning_content and OpenCode saved it correctly. I didn't capture an affected turn, so it's still open whether those responses contain reasoning that OpenCode drops or none at all. I also couldn't check the latest dev against this model, because Zen's free tier refuses requests from a source build. 2.0.27 is the latest release.

    Related issues, for reference:

    Reproduction script (repro.sh)
    #!/usr/bin/env bash
    # Repro for #54886: run N one-shot turns with opencode/big-pickle (free Zen model),
    # then scan the local DB for assistant messages with tokens.reasoning > 0 but no reasoning part.
    # Usage: OC="opencode" ./repro.sh 15   (or OC="bun ... packages/cli/src/index.ts")
    set -u
    N=${1:-15}
    OC=${OC:-opencode}
    mkdir -p /tmp/oc54886 && cd /tmp/oc54886
    for i in $(seq 1 "$N"); do
      timeout 150 $OC run --standalone -m opencode/big-pickle "Write a short haiku about number $((i+200)), thinking carefully first." >/dev/null 2>&1
    done
    python3 - <<'PY'
    import sqlite3, json, os
    c = sqlite3.connect(os.path.expanduser('~/.local/share/opencode/opencode.db'))
    cols = [r[1] for r in c.execute("pragma table_info(session_message)")]
    total = bad = 0
    for row in c.execute("select * from session_message"):
        d = dict(zip(cols, row))
        for k, v in list(d.items()):
            if isinstance(v, str) and v.startswith('{'):
                try: d.update(json.loads(v))
                except Exception: pass
        content, tok = d.get('content'), d.get('tokens')
        if not isinstance(content, list) or not isinstance(tok, dict) or not tok.get('reasoning'): continue
        total += 1
        if not any(p.get('type') == 'reasoning' for p in content):
            bad += 1
            print('MISSING reasoning part:', d.get('session_id'), 'tokens.reasoning =', tok['reasoning'], 'parts =', [p.get('type') for p in content])
    print(f'{bad}/{total} assistant messages with tokens.reasoning > 0 have no reasoning part')
    PY

    Run it: bash repro.sh

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions