Build1 publisher2 min readPublished
Ollama's JSON decoder drops previous_response_id before any handler sees it
A single-box test on Ollama 0.34.0 shows a turn chained with previous_response_id coming back HTTP 200 and status completed at the same 41 input tokens as the same question sent with no history at all. The request struct has no field for the key.
The Engineer · Build desk

What happened
- A second turn sent to Ollama 0.34.0 with previous_response_id resp_578667 returned HTTP 200, status completed and error null, then answered "password" when the first turn had planted PINEAPPLE.
- In a tool-calling loop, the turn after a function_call_output reported that the test passed successfully instead of the exact DONE the instruction it never saw had demanded.
- The response struct does declare previous_response_id, assigns nil to it with the comment Not supported, and every reply therefore reports the field as null.
Compiled by The EngineerSomething wrong?How this is made
Why it matters
- exposure A client that trusts server-side threading gets a well-formed completed reply on every turn, so the failure never reaches an error handler and shows up only as answers that do not follow instructions.
- constraint With no error to alert on, detection has to come from token accounting: compare a chained turn's input_tokens against the same turn sent with no history and see whether the two match.
- cost Correct behaviour on Ollama means resending the whole conversation in the request body, and the client pays that on every turn: 27 extra input tokens in the secret-word test, 35 in the tool loop.
Go's encoding/json drops object keys that have no matching struct field, and it only errors on them when the decoder has DisallowUnknownFields set. In Ollama 0.34.0's openai/responses.go that option is not set, and ResponsesRequest declares Model, Input, Instructions and Tools with no PreviousResponseID field [11][12]. The key dies at decode [12]. Set the option and the same request fails at the decoder instead [19].
The token counts are the only in-band evidence a client gets, and in the single-box test published on dev.to they line up cleanly. Turn 1 planted a word the model could not guess, at 45 input tokens [3]. Turn 2, sent with previous_response_id resp_578667, cost 41 [4]. The same question with no history at all also cost 41 [6][16]. Sending the three-message history in the request body cost 68 and returned PINEAPPLE [5], so the conversation the chained request claimed to inherit was worth 27 input tokens the server never counted [17]. In the tool-calling test the gap was 35, 144 against 179 [9][10][18].
The answers are what hides it. Asked for the planted word with previous_response_id set, the model returned password [4]. Through /api/codex/v1/responses, the proxy path that ollama launch codex sets up for Codex Desktop, it returned Qwen, also at 41 tokens, also completed [7]. In the tool loop the instruction was to reply exactly DONE; after the function_call_output went back, the reply was "The test passed successfully! Is there anything else you need help with?" [9]. The full-history control replied DONE [10].
The response body does know the field exists. openai/responses.go declares PreviousResponseID *string with the json tag previous_response_id and assigns nil to it, with the comment // Not supported [13]. Every reply therefore reports previous_response_id as null, which is accurate.
What transfers off that box is the decode behaviour, because it lives in the request struct and does not depend on the model, the hardware or the quantisation [11][12]. The counts do not transfer: 41 and 68 are qwen2.5:1.5b's tokenizer on those prompts, CPU only, on Debian 13 [2][4][5]. Neither does the answer, which is where the hosted case parts company with the local one. The upstream report that prompted the test used a hosted :cloud model through the same Codex proxy and got an empty output_text with input_tokens 0 for the same bare function_call_output, which Codex Desktop treated as a cleanly finished turn [14]. The author did not test a hosted model, and that symptom did not reproduce locally [15]. Both shapes reach the client through the same path: 200, completed, error null [1][20].
What to watch
- Whether Ollama adds a PreviousResponseID field to ResponsesRequest, or sets DisallowUnknownFields so the key becomes a 400.
- Whether the hosted :cloud path reproduces the empty output_text with input_tokens 0 for a bare function_call_output.
- Whether Codex Desktop surfaces anything when a response comes back with previous_response_id null.