23 KiB
Responses API Continuation Plan
This note tracks the design for fixing /v1/responses tool-call continuation
and the KV-cache behavior around it.
Problem
The current implementation handles /v1/responses too much like a stateless
chat-completions replay API. After a tool call, it tries to canonicalize the
live KV checkpoint so it matches the next rendered prompt byte-for-byte.
That is wrong for the live Responses protocol path.
In a live Responses tool loop, the next request is not an unrelated prompt. It is a continuation bound to the previous model output by tool-call ids. The live KV already contains the true assistant turn, including hidden reasoning that the client may not replay. Rebuilding the session to match only the visible client transcript is both slow and less faithful to the state the model actually produced.
Observed symptom:
tool checkpoint canonicalization needs rebuild ... common=16846 live=16994 canonical=16937
tool checkpoint canonicalized ... via=rebuild
This caused a long pause because DS4 rebuilt roughly the whole context before returning the streamed tool-call response tail.
Protocol Model
There are two useful id classes:
previous_response_id/conversation: response-level server-side state. DS4 does not currently persist this. If a non-null value arrives, return an error asking the client to replay full input.call_id: tool-call binding. Afunction_call_outputor hosted-tool output with this id is the continuation of the previous assistant tool call.
The server should distinguish two modes:
- Live continuation: the request contains tool outputs for tool-call ids still known by the live server state. Continue from the live KV state and append the new tool-result suffix.
- Stateless replay: the request includes the full history. Render and match the best prefix, using exact DSML replay from tool-memory when possible, just like chat-completions style replay.
Unknown tool-output ids are only valid if the request also includes the matching prior function-call item in the replayed history. If neither live state nor full history can explain the id, return an error.
Core mental model:
- The Responses API is designed so a server can avoid matching an already-known prefix. If the request is tied to live server state by response/conversation state or by immediately returning tool outputs for live call ids, DS4 should continue from the live KV instead of proving that the client-visible replay tokenizes to the same prefix.
- Prefix matching is only the fallback for stateless replay, cold start, server restart, branch/edit, or multi-client cases where the server no longer has the relevant live state.
- The fallback should reuse the techniques learned for chat continuation: exact token-prefix match first, then rendered string-prefix match with suffix retokenization, then disk string-prefix checkpoints. The adaptation for Responses is that reasoning/tool-call stateless replay must include the reasoning state the protocol requires, not just the visible transcript.
Official OpenAI Docs Findings
Sources checked:
- Conversation state:
https://developers.openai.com/api/docs/guides/conversation-state - Function calling:
https://developers.openai.com/api/docs/guides/function-calling - Responses API reference:
https://platform.openai.com/docs/api-reference/responses
Findings that affect the implementation:
- The Responses API has two explicit server-side state mechanisms:
previous_response_idandconversation.previous_response_idchains a new response to a stored prior response.conversationprepends persisted conversation items and then appends new input/output items after the response completes.- They are mutually exclusive in the API reference.
- Stateless/manual Responses state is also valid, but the client must preserve
the model output items. The function-calling guide shows:
- Start with an
inputlist. - Call
responses.create(). - Append
response.outputto the same input list. - Append
function_call_outputitems with the matchingcall_id. - Send the resulting input list back.
- Start with an
- For reasoning models, the function-calling guide explicitly says that any reasoning items returned in model responses with tool calls must also be passed back with tool-call outputs.
- The Responses API reference exposes
include: ["reasoning.encrypted_content"], described as enabling reasoning items to be used in stateless multi-turn conversations whenstore=falseor ZDR applies.
Implication for DS4:
- A request containing only a function-call output is not enough to recreate stateless reasoning context unless DS4 still has live state for that call id.
- A full stateless replay should include the prior function/custom tool call item and, for reasoning/tool-call turns, the reasoning item or an equivalent opaque reasoning-state item.
- DS4 currently does not implement durable
previous_response_idorconversationstate, so non-null values should remain rejected unless we add a real response/conversation store. - The current Codex-style
store=false/include=[]flow can still work as a live local server fast path, but if the live state is gone DS4 should reject and ask for a full replay that includes the missing reasoning state, rather than silently rebuilding a prompt with hidden reasoning removed.
Current Code Facts
- Exact DSML replay via
tool_memory/ rax is still valid and working. It fixes the visible tool-call block by replaying the sampled DSML for known call ids. - The bad path is
canonicalize_tool_checkpoint()for Responses tool calls when reasoning summaries are not emitted. It drops hidden reasoning and can trigger full rebuilds. live_text_prefix_prompt()is still useful for ordinary replay/cache matching, but it is not enough for the live Responses continuation case because the client-visible text can omit hidden reasoning that exists in the live KV.ds4_session_save_snapshot()/ds4_session_load_snapshot()exist, but they should not be the primary continuation mechanism for this protocol path.
Implementation Plan
-
Track live Responses continuation state.
- Remember the latest Responses assistant tool-call turn after generation.
- Store the generated call ids and enough rendered visible text to recognize the next request.
- Keep the live KV as authoritative; do not canonicalize away hidden reasoning for this path.
- Treat this as an optimization for the current live single-server session,
not as a substitute for
previous_response_id/conversation. - Prefer this path for Codex/pi tool loops when integration tests show those clients send tool outputs that are directly bound to the previous live call ids.
-
Parse Responses tool outputs with validation.
- Collect function/custom/hosted tool-output ids during
parse_responses_input(). - Collect function/custom/hosted call ids present in the replayed history.
- For reasoning-mode tool-call turns, distinguish a true full replay from a visible-only replay. A matching prior call id without the reasoning item is enough to render visible history, but not enough to reconstruct hidden model state if live continuation is unavailable.
- Reject tool-output ids that are neither known live ids nor present as prior calls in the same request history.
- Collect function/custom/hosted tool-output ids during
-
Add a live continuation fast path before normal cache matching.
- If the request is a Responses request and its first new semantic item is a tool output for the remembered live call ids, build only the suffix that must be appended to the current live KV.
- Tokenize that suffix independently from the live token prefix, as the existing text-prefix path does, to avoid BPE boundary mistakes.
- Set
prompt_for_synctolive_tokens + suffix_tokens, withcached = live_tokens->len.
-
Keep stateless replay behavior.
- If the request is not a live continuation, render the full prompt and use the existing token-prefix, live text-prefix, and disk text-prefix cache paths.
- Exact DSML tool replay remains available to improve matching.
- If the request is a stateless reasoning/tool-call replay without reasoning items or equivalent opaque reasoning state, reject instead of pretending the visible transcript is complete.
-
Remove invalid Responses canonicalization.
- Do not call
canonicalize_tool_checkpoint()for Responses live tool-call continuations. - Remove the Responses-specific reasoning-dropping behavior in
build_tool_checkpoint_suffix(). - Keep or narrow toolless thinking canonicalization only where the next request is truly a stateless replay and the live hidden reasoning cannot be continued by protocol ids.
- Do not call
-
Improve errors.
- Non-null
previous_response_id/conversation: return a clear 400 error unless DS4 later implements persistent response state. - Unknown tool-output id without a matching prior function call in the same request: return a clear 400 error explaining that DS4 needs full history.
- Reasoning/tool-call replay that lacks reasoning state and is not live: return a clear 400 explaining that DS4 needs either live continuation or a full Responses replay with reasoning items / encrypted reasoning content.
- Non-null
Tests
Automated tests:
- Responses live tool continuation keeps hidden reasoning in live KV and appends only the tool-result suffix.
- Responses live continuation does not call tool checkpoint canonicalization.
- Unknown tool-output id with no prior function call is rejected.
- Unknown tool-output id with a prior function call in replayed history is accepted as stateless replay only when the replay has enough reasoning state for the selected mode.
- Reasoning/tool-call stateless replay without reasoning state is rejected if live continuation is unavailable.
- Normal chat-completions exact DSML replay still works.
- Disk KV tool-map restore still works for stateless replay.
- BPE boundary handling remains correct for live-prefix plus text suffix.
Live tests:
- Codex multi-turn session with repeated tool calls.
- Expected: after each tool call, next request reports a memory-token continuation or equivalent live continuation, not a rebuild.
- Expected: no long pause after
finish=tool_calls. - Verify the actual client behavior: whether Codex sends
previous_response_id, fullresponse.output, function-call outputs only, reasoning items, or some mixture.
- Pi agent multi-turn session with tools.
- Expected: tool calls execute and the agent recovers normally from tool outputs.
- Expected: no unexpected cache misses.
- Verify the actual client behavior independently from Codex. Do not assume both clients use the same Responses replay style.
- Stateless replay request with full history and known call ids.
- Expected: exact DSML replay via rax and best-prefix cache matching.
- Bad continuation request with only a tool output for an unknown id.
- Expected: HTTP 400 with a direct "replay full history" style message.
Current Progress
- Confirmed from trace that the pause is synchronous checkpoint canonicalization and rebuild after a parsed tool call.
- Confirmed rax exact DSML replay is not the failing part:
tool_replay: mem=4 disk=0 canonical=0 missing_ids=0. - Confirmed the current Responses parser rejects non-null
previous_response_id/conversation, which is the right behavior until server-side response persistence exists. - Checked official OpenAI documentation. The plan was updated to account for the documented requirement that reasoning-model tool-call loops preserve reasoning items in stateless manual replay.
- Implemented live Responses continuation state and removed Responses tool-call checkpoint canonicalization from the main path.
- Added normal server logs for accepted live Responses continuations:
responses live continuation match=... cached=... prompt=.... Tool-output continuations now also report the matched id count, for examplematch=visible-prefix ids=1. - Added Responses-visible disk checkpoint keys. The payload is still the exact live KV with hidden reasoning, but the file name / lookup text is the visible transcript the client can replay. This is required when switching between Codex, pi, and synthetic clients, because only one live KV session can be resident at a time.
QA Log
All tests in this section were run on the local M3 Max server with Metal,
--ctx 65536, and /v1/responses clients configured for
deepseek-v4-flash.
Automated checks:
make ds4_test && ./ds4_test --server && make ds4-server: pass.- Added unit coverage for unknown tool-output ids, reasoning-required stateless replay, Responses live-tail rendering, and final-answer visible suffix handling.
Official protocol checks:
previous_response_id != null: returns HTTP 400 withprevious_response_id is not supported; replay full input instead.conversation != null: returns HTTP 400 withconversation is not supported; replay full input instead.- Tool output for an unknown
call_id: returns HTTP 400 withunknown tool output call_id ...; replay full Responses history. - Stateless replay with a prior tool call but no reasoning item: returns HTTP
400 with
Responses replay is missing reasoning state.... - Stateless replay with a prior tool call and an explicit reasoning item: returns HTTP 200 and generates from the replayed history.
Codex live behavior:
- Codex sends
store=false,include=[],prompt_cache_key=<session id>, and full visible replay. - Exact token matching fails at hidden thinking, as expected:
live_prompt_commonstops where live KV has hidden reasoning and the visible replay has</think>. - Live visible continuation now catches this path:
cache_source: responses-visible, with short suffix prefill after tool outputs and after normal resumed user turns. - Repeated Codex turns completed with tool execution and no
canonicalization needs rebuildpause.
Pi live behavior:
- Pi uses a custom local provider with
api: openai-responses. - Pi sends
reasoning.summary=autoandinclude: ["reasoning.encrypted_content"]. - Pi replays reasoning summaries for assistant tool-call turns, but not for final assistant text answers. The visible-prefix state must therefore include tool-call reasoning summaries when present, but omit final-answer hidden reasoning.
- Pi first turn, tool result, resumed user turn, and resumed tool result all
completed. Accepted live continuations print:
responses live continuation match=visible-prefix ...; tool-output turns printids=1in the normal server log.
Interleaved session switching:
- Sequence tested: Codex turn, pi turn, Codex resume, pi resume, Codex resume, plus synthetic curl traffic between agent turns.
- On switching clients, the live KV miss is expected and logged in orange.
- Before replacing live state, DS4 stores the outgoing Responses session as
key=responses-visible. - Returning to Codex after pi loaded the Codex checkpoint from disk:
kv cache hit text tokens=11772 ..., then prefills only the new suffix. - Returning to pi after Codex loaded the pi checkpoint from disk:
kv cache hit text tokens=1654 ..., then prefills only the new suffix. - Returning to Codex after synthetic curl traffic again loaded the latest Codex
checkpoint from disk:
kv cache hit text tokens=12211 .... - The earlier failure mode was reproduced before the fix: Codex resume after
pi received
Responses replay is missing reasoning state...because the guard ran before disk recovery and the disk key was raw hidden text. The fix is to key Responses checkpoints by visible transcript and accept onlyKV_EXT_RESPONSES_VISIBLEdisk hits for missing-reasoning replay.
Remaining risks / follow-up:
- DS4 still does not implement durable
previous_response_idorconversation; rejecting non-null values remains intentional. reasoning.encrypted_contentis not decrypted. It is useful for real OpenAI stateless clients, but DS4 currently relies on live state or visible-key disk checkpoints for hidden reasoning recovery.- The tool-output-only continuation path is unit-tested, but the observed Codex and pi clients both used full visible replay in their normal runs.
- Tool-output-only continuation was also exercised directly with curl:
first request produced
call_3c4891795bf43aca507f4a987707b259, second request sent onlyfunction_call_outputfor that id, and the server accepted it asresponses live continuation match=tool-output-ids ids=1. - Code review follow-up tightened a race-shaped edge case: if a request contains only tool outputs, it is marked as requiring the live call-id state. If that state no longer matches by worker execution time, DS4 now returns a 400 asking for full input instead of cold-prefilling a prompt that starts with a naked tool result. Full visible replays in thinking mode also remain marked as reasoning-sensitive even if live state existed during parse, because the live KV can be replaced before execution.
- Disk cache hit logs now include the key kind:
key=responses-visiblefor visible-transcript keys that restore hidden KV, andkey=token-textfor ordinary rendered-token text keys. make ds4_test && ./ds4_test --server && make ds4-server: pass after the stricter tool-output-only check and log-key change.- A separate
/tmp/ds4_test_asanbuild with-fsanitize=address,undefinedalso passes--server. Leak detection is not supported by Apple ASan on this platform, so the run covered address/UB checks but not leak reporting.
Restart / disk recovery test:
- Stopped the old server and started a fresh
ds4-serverwith the existing/tmp/kvcache-responses-switch3directory. - Resumed the existing Codex session
019e23a9-821c-7e40-8c00-122646d7fda7after process restart. - Expected behavior occurred immediately:
kv cache hit text tokens=12367 ... key=responses-visible, followed by a short suffix prefill and a live Responses tool-output continuation withids=1. - After unrelated pi, curl, and opencode traffic replaced live KV, resuming
Codex again hit disk with
key=responses-visibleat the later checkpoint and then continued the tool result live. This confirms the cross-client/session path is not only an in-process memory shortcut. - Shutdown persistence was also tested: stopping the server logged
reason=shutdown key=responses-visiblefor the latest Codex live state. After a fresh restart, resuming the same Codex session hit that file:kv cache hit text tokens=12897 ... key=responses-visible, then completed the next tool call and tool-result turn normally.
Additional live clients:
- Pi was resumed from the saved session file and completed a real bash tool
turn. In this invocation pi sent a shorter prompt for the new turn, so DS4
cold-prefilled that request and then accepted the tool result through
responses live continuation match=visible-prefix ids=1. - opencode was run for two real shell-tool turns against the configured
ds4/deepseek-v4-flashprovider. This path uses the OpenAI-compatible chat API rather than/v1/responses, and it completed both the initial tool call and the resumed tool call. That is useful shared-path regression coverage: chat/completions tool checkpoint canonicalization still works while Responses skips it.
More protocol negatives:
- Non-null
previous_response_id: still HTTP 400. - Non-null
conversation: still HTTP 400. - Tool output with an unknown
call_id: still HTTP 400. - Stateless replay of a prior tool call plus tool output, without a reasoning item in thinking mode: HTTP 400.
- The same stateless replay shape with an explicit
reasoningitem: HTTP 200. - The short tool-output-only happy path was re-tested after the stricter guard:
first curl request generated a
bashfunction_call, second request sent only the matchingfunction_call_output, and DS4 loggedresponses live continuation match=tool-output-ids ids=1. - The stale-live-state failure path was also tested with real queued requests:
create a live
bashfunction_call, start a long unrelated request that replaces the live KV, then submit the old tool-output-only request while the long request is still running. When the queued tool-output-only request executed, DS4 returned HTTP 400 withResponses tool output requires live call state; replay full input instead. This verifies the new guard prevents cold-prefilling an orphan tool result.
Post-merge stress pass, 2026-05-14:
- Merged the
responses-apibranch intomainwhile preserving its branch commits, then re-applied the exact sampled DSML tool-checkpoint gate on top of the merged Responses live-continuation path. - Replaced eager tool-less thinking checkpoint rebuilds with a visible-transcript
live binding. A no-tool thinking answer now remembers the visible transcript
that clients replay, while keeping the sampled hidden reasoning in KV. The
next chat/completions or Anthropic request can continue with
cache_source: thinking-visibleif its prompt extends that visible prefix. - Unit/build checks after the change:
make ds4_test,./ds4_test --server, andmake ds4-serverall passed. - Live stress server:
./ds4-server -m gguf/DeepSeek-V4-Flash-IQ2XXS-w2Q2K-AProjQ8-SExpQ8-OutQ8-chat-v2-imatrix.gguf --ctx 100000 --kv-disk-dir /tmp/ds4-kv-stress --kv-disk-space-mb 8192 --trace /tmp/ds4-response-stress-trace.txt. - Pi, using the OpenAI-compatible chat/completions path, completed a multi-tool
session and a resumed turn. The trace showed normal live-prefix reuse:
requests after the first cold prompt used
cache_source: memory-tokenwith only short suffix prefills. - Codex CLI, using
/v1/responses, completed one tool-heavy turn and a resumed turn. Responses continuations usedcache_source: responses-visible; the first resume after the final answer loggedids=0and continued from the remembered visible prefix, then tool-result turns loggedids=1. - Claude Code, using
/v1/messages, completed real read/write/bash tool loops. One apparent bad miss after aReadcall was investigated in the rendered trace: the client changed its tool schema block between requests (NotebookEditdisappeared andLSPappeared), so the low common prefix was a real prompt change, not a DSML replay failure. Disk recovery limited the replay to the nearest checkpoint (cached_tokens=22528for a 24308-token prompt). - opencode, using OpenAI-compatible chat/completions, completed a tool session
and a resumed tool session. Most tool-result turns used live memory; a few
used
cache_source: memory-textbecause the exact token stream and rerendered byte prefix were text-identical but had different tokenization around a boundary, which is the intended text-prefix fallback. - A direct no-tool chat/completions test exercised the new thinking-visible
binding: first request logged
thinking live checkpoint remembered, second request loggedthinking live continuation match=visible-prefix cached=54 prompt=66, with no rebuild and only the new suffix prefilling. - No trace line in this run showed
thinking checkpoint canonicalization needs rebuildor a tool checkpoint rebuild caused by exact sampled DSML replay.