Preserve tool-call history across a JSON save and restore
A runnable Python example checks what survives when tool-call history is saved to JSON and restored, including assistant fields and tool result IDs.

A chat view can survive a restart even when the history used for the next request has lost required fields. This matters when a paid coding tool saves conversations, restarts its worker and reconstructs a tool-enabled request from storage.
This tutorial isolates that boundary with an original Python fixture. It saves JSON to a temporary file, reads it back and compares the reconstructed messages. All values are synthetic. There is no API client, network request, real function execution or model-generated reasoning in the example.
Establish the route contract first
As checked on 30 October 2026, DeepSeek's thinking-mode guide says a request carrying tools should return reasoning_content from every previous turn. Without tools, the guide says historical reasoning_content can be omitted. These are different request conditions; the absence of a tool call in an earlier answer does not itself select the second condition.
The IteraCompute Flash 1017 route page separately describes preserving assistant history during tool use and returning tool results with their original call IDs. Treat that page as a gateway declaration. This tutorial has not established that the gateway accepts every payload accepted by the direct provider.
DeepSeek's tool guide also places function execution in the application. A call recorded in a message is not evidence that its function ran. The fixture below therefore uses invented result strings and does not execute either named function.
Separate display data from request history
A display layer might need only role and content. A storage adapter that saves only those two fields can make a conversation look intact while removing reasoning_content, tool_calls or tool_call_id.
Use a single invented example with two assistant messages. The first has no tool call. The second has null content, an opaque sentinel and two declared calls. Two following result messages carry the corresponding IDs. The sentinel strings are authored markers for preservation checks; they do not reproduce a model's private reasoning.
The local contract in this tutorial is deliberately strict: the decoded list after saving must equal the original decoded list. Object key order is irrelevant to Python dictionary equality; message order, string values and the distinction between null and an empty string remain significant. A second check compares declared and returned IDs and rejects duplicates. Neither check constitutes a general API schema validator.
Run the complete offline example
Save the following as history_demo.py and run python3 history_demo.py with Python 3.10 or later. It uses only the standard library. The temporary synthetic file is removed when execution finishes. The original code is available under CC0-1.0; that license does not cover the company brand marks in the separate illustration.
#!/usr/bin/env python3
"""CC0 original offline fixture. Synthetic fields, no HTTP, SDK or model calls."""
from copy import deepcopy
import json
from pathlib import Path
from tempfile import TemporaryDirectory
# Authored opaque sentinels: these are not model-generated reasoning.
original = [
{"role": "user", "content": "Synthetic inventory question A"},
{"role": "assistant", "content": "Synthetic answer A",
"reasoning_content": "SENTINEL_A"},
{"role": "user", "content": "Synthetic inventory question B"},
{"role": "assistant", "content": None,
"reasoning_content": "SENTINEL_B",
"tool_calls": [
{"id": "call_a", "type": "function", "function": {
"name": "read_fixture", "arguments": '{"item":"A"}'}},
{"id": "call_b", "type": "function", "function": {
"name": "read_fixture", "arguments": '{"item":"B"}'}}]},
{"role": "tool", "tool_call_id": "call_a", "content": "1"},
{"role": "tool", "tool_call_id": "call_b", "content": "2"},
]
def check(before, after):
# Strict local preservation contract, deliberately stronger than API acceptance.
errors = [] if before == after else ["payload_changed"]
calls = [c["id"] for m in after for c in m.get("tool_calls", [])]
results = [m.get("tool_call_id") for m in after if m["role"] == "tool"]
if len(calls) != len(set(calls)):
errors.append("duplicate_call")
if None in results:
errors.append("missing_result_id")
results = [r for r in results if r is not None]
if len(results) != len(set(results)):
errors.append("duplicate_result")
if set(calls) != set(results):
errors.append("result_ids_mismatch")
return errors
def save_load(messages, folder):
path = Path(folder) / "synthetic-history.json"
path.write_text(json.dumps(messages, ensure_ascii=False), encoding="utf-8")
return json.loads(path.read_text(encoding="utf-8"))
def main():
with TemporaryDirectory() as folder:
restored = save_load(original, folder)
print("complete:", check(original, restored))
# A display projection is readable but drops the wire-only fields.
projected = [{k: m[k] for k in ("role", "content")} for m in original]
lossy = save_load(projected, folder)
print("display-only:", check(original, lossy))
changed = deepcopy(restored)
changed[-1]["tool_call_id"] = "orphan"
print("wrong-result-id:", check(original, changed))
if __name__ == "__main__":
main()Expected output from the code executed locally for this article:
complete: []
display-only: ['payload_changed', 'missing_result_id']
wrong-result-id: ['payload_changed', 'result_ids_mismatch']The first result shows that this particular JSON file round trip preserved the invented list. The second result identifies a lossy display projection. In the third, one changed ID leaves the declared and returned sets inconsistent. The checker reports what changed in this fixture; it does not contact a provider or reproduce an HTTP 400.
Give the storage boundary an acceptance record
Before using this method in a real integration, replace the temporary file adapter with an authorized non-production instance of the actual persistence layer. Keep a synthetic fixture. Record the exact route, request mode, serializer version, storage schema version and the comparison result. Assign an owner to the first boundary that changes the payload.
Check a normal restart and any migration that rebuilds historical records. A new serializer can change which extension fields are retained even when the model ID stays unchanged. A database migration can coerce null to an empty string, strip an unfamiliar key or rebuild a result with a fresh ID. Comparing only the rendered conversation will miss those changes.
A useful handoff distinguishes three observations: the messages before saving, the decoded messages after restoring and the actual outbound request assembled from them. Passing a file round trip covers only the middle boundary. Request builders and SDK serialization require their own evidence. Do not attach real credentials or private customer histories to a public bug report; an invented fixture is enough to demonstrate this class of loss.
Understand what this checker leaves open
The accompanying local controls separately removed old and current reasoning markers, changed null content, rewrote arguments, removed a result, changed an ID, duplicated a call or result, changed result text, dropped a user message, reordered the two results and projected the display fields. A further control supplied an unchanged payload with already-invalid pairing. All 14 hand-derived controls produced the expected local diagnostics.
That count measures checks of this authored example. It does not measure model accuracy, recovery success, throughput or provider conformance. Reordering results is rejected by the strict equality contract here; that is not a claim that every provider rejects the same ordering. The ID checker uses one global set for this one tool batch. It does not validate call/result ordering across several batches, schemas, token limits, streaming assembly, side effects or exactly-once execution.
Temporary JSON storage also says nothing about database transactions, durability, concurrent writers, access control, retention or data deletion. Preserve fields only within the applicable authorized data-handling policy; this example is not a recommendation to publish reasoning content or retain it indefinitely. No gateway request, actual HTTP error, customer trace, bill, latency or quality result was collected.
Compare the route's current protocol with the fields your persistence adapter restores: read the provider's history requirements.