Skip to content

deepseek-v4-flash-free (zen free tier): stream drops the final text delta before tool_calls; non-streaming is complete #40959

Closed as not planned

Description

@carlosmintfan

Description

deepseek-v4-flash-free via https://opencode.ai/zen/v1/chat/completions (free tier) drops the final text delta before tool_calls from streamed responses: the sentence before the tool call ends mid-word, no error, finish_reason: "tool_calls". The same request with stream: false returns the complete text. Non-deterministic — identical requests sometimes complete.

Plugins

None — raw HTTP, no client SDK involved.

OpenCode version

N/A (API-level; also reproduced via Vercel AI SDK streamText, pi's SDK streamSimple, and headless opencode run)

Steps to reproduce

repro.sh — curl + python3, no other deps:

#!/usr/bin/env bash
# Usage: ./repro.sh [runs]
set -u
KEY="${OPENCODE_API_KEY:-public}"          # free tier accepts "public"
N="${1:-8}"
PHRASE="The quick brown fox jumps over the lazy dog"

req() { # $1 = stream(true/false)
  curl -sS --max-time 90 \
    -H "Authorization: Bearer $KEY" -H "Content-Type: application/json" \
    -d "{\"model\":\"deepseek-v4-flash-free\",\"messages\":[{\"role\":\"user\",\"content\":\"Say exactly '$PHRASE' then call the tool get_time with an empty object.\"}],\"stream\":$1,\"tools\":[{\"type\":\"function\",\"function\":{\"name\":\"get_time\",\"parameters\":{\"type\":\"object\",\"properties\":{}}}}]}" \
    "https://opencode.ai/zen/v1/chat/completions"
}

stream_text() {
  python3 -c '
import sys, json
text = ""
for line in sys.stdin:
    line = line.strip()
    if not line.startswith("data:"): continue
    p = line[5:].strip()
    if p == "[DONE]": continue
    try: o = json.loads(p)
    except Exception: continue
    for c in o.get("choices", []):
        d = c.get("delta") or {}
        if isinstance(d.get("content"), str): text += d["content"]
print(text)'
}

trunc=0
for i in $(seq 1 "$N"); do
  body=$(req true)
  id=$(printf '%s' "$body" | grep -o '"id":"[^"]*"' | head -1 | cut -d'"' -f4)
  text=$(printf '%s' "$body" | stream_text)
  if [[ "$text" == *"lazy dog"* ]]; then s="complete"
  elif [[ "$text" == *"fox"* ]]; then s="TRUNCATED"; trunc=$((trunc+1))
  else s="no-text"; fi
  printf 'run %2d  id=%-22s  %-9s  ...%s\n' "$i" "${id:0:22}" "$s" "${text: -32}"
  sleep 0.3
done
echo
echo "streaming: $trunc/$N truncated (expected tail: '$PHRASE')"
echo "--- non-streaming control (stream:false) ---"
req false | python3 -c 'import sys,json; o=json.load(sys.stdin); print("content:", o["choices"][0]["message"]["content"])'

Screenshot and/or share link

Output of one run:

run  1  id=router-c651e84ac345a25  TRUNCATED  ...e quick brown fox jumps over the
run  2  id=router-4b3abcc145d862d  TRUNCATED  ...e quick brown fox jumps over the
run  3  id=router-59f37695f6c301f  TRUNCATED  ...
run  4  id=router-906ca39fd5009d0  TRUNCATED  ...e quick brown fox jumps over the
run  5  id=router-4f990f930cbd9cd  TRUNCATED  ...e quick brown fox jumps over the
run  6  id=b7ea5f8c-1b71-4d5a-9d3  complete   ...rown fox jumps over the lazy dog
run  7  id=router-87d80b13af7041b  TRUNCATED  ...e quick brown fox jumps over the
run  8  id=router-b0831d8d0f852b9  TRUNCATED  ...e quick brown fox jumps over the

streaming: 7/8 truncated (expected tail: 'The quick brown fox jumps over the lazy dog')
--- non-streaming control (stream:false) ---
content: The quick brown fox jumps over the lazy dog

Second run: streaming: 6/6 truncated, non-streaming control complete.

Operating System

Linux (container); client-independent

Terminal

N/A

Additional evidence

  • Raw SSE of a truncated run (verbatim capture, no parser): content='The' → content=' quick brown fox jumps over the' (last content delta) → tool_calls → finish_reason: tool_calls → [DONE]. The tail ' lazy dog' never appears as a content delta. The full sentence IS present in the same stream's reasoning_content deltas and in the stream: false response — the model generated it; the SSE dropped it.
  • Truncation correlates with the response chunk id prefix. Across ~100 streamed runs with tools: every truncated response had a router-* id; uuid-prefixed responses were always complete. In the systematic matrix: 28/33 router-* runs truncated (~85%), 0/14 uuid runs.
  • Request parameters don't change it: reproduced with/without temperature, x-opencode-session, stream_options, max_tokens, and different tool-schema shapes.
  • Not a parsing artifact: with the Vercel AI SDK and pi's SDK, the SDK's emitted text matched the captured raw bytes byte-for-byte (both truncated).

Related: #40888 (streaming + tools truncated on the Go tier), #40176 (deepseek-v4-flash on Go drops the tool call after text — same text↔tool transition defect, opposite side).

Activity

  1. kasiro commented on Aug 16, 2026

    @kasiro

    Confirmed — reproduced independently on 2026-08-17 against opencode.ai/zen/v1 with deepseek-v4-flash-free:

    • stream: true + tools → content truncated mid-word in 3/3 runs (finish_reason: tool_calls)
    • stream: false with the same request → complete text

    Practical impact: OpenCode 1.18.18 (current) always streams via the AI SDK, so every agent text that precedes a tool call is truncated — the final word/letters are missing. This is visible in stored session data.


    Sent by: deepseek-v4-flash-free (Linux)

  2. github-actions commented on Oct 16, 2026

    @github-actions
    Contributor

    To stay organized issues are automatically closed after 60 days of no activity. If the issue is still relevant please open a new one.

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

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions