Skip to content

feat(chat_models): record the response id and chain calls with use_previous_response_id - #349

Open
Akhil Jasson (Akhil-Jasson) wants to merge 4 commits into
langchain-ai:mainfrom
Akhil-Jasson:feat/responses-api-previous-response-id
Open

Akhil Jasson (Akhil-Jasson) wants to merge 4 commits into
langchain-ai:mainfrom
Akhil-Jasson:feat/responses-api-previous-response-id

Conversation

@Akhil-Jasson

Copy link
Copy Markdown

Fixes #305

Summary

Adds full Responses API response-id chaining for ChatLiteLLM and ChatLiteLLMRouter: Responses API replies now expose LiteLLM’s upstream resp_... id in AIMessage.response_metadata["id"] for sync, async, streaming, and non-streaming calls. Adds use_previous_response_id=True, which finds the most recent response id in message history, sends it as previous_response_id, omits already server-stored history, and forces the request through LiteLLM’s Responses API route; also raises the LiteLLM minimum to 1.102.0, the first version that returns the upstream id.

Verification

  • python -m compileall -q langchain_litellm tests/unit_tests/test_litellm.py tests/unit_tests/test_router.py
  • git diff --check

Added focused unit coverage for direct and Router chaining, response-id propagation, and streamed response ids.

@github-actions github-actions Bot added the feat New feature or capability label Oct 1, 2026
@Akhil-Jasson
Akhil Jasson (Akhil-Jasson) force-pushed the feat/responses-api-previous-response-id branch from 447967c to 1de8ee1 Compare October 1, 2026 11:54

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks, the id on every path of both classes and the 1.102.0 floor are right: the new id assertions fail on litellm 1.101.0 and pass on 1.102.0. use_previous_response_id needs these before I can merge.

Blocking

  1. Chained turns lose the system prompt. litellm's Responses bridge sends a leading system message with str content as instructions, and instructions are not carried over by previous_response_id (see the instructions docstring on openai's ResponseCreateParams). The trim in _last_messages_with_response_id (litellm.py:131) removes it, so from turn 2 on the model runs without its system prompt. ChatOpenAI gets away with the same trim only because it sends system messages as stored input items. Keep every message in the trimmed prefix that _convert_message_to_dict turns into role system with str content (SystemMessage and ChatMessage(role="system")), ahead of the kept turns. List content already goes as a stored input item, so leave that out.
  2. Only chain on resp_ ids. This PR puts an id on every reply, so a Chat Completions reply in the history (chatcmpl-..., or '' from an OpenAI-compatible stream) becomes previous_response_id, and everything before it is dropped. Match ChatOpenAI: chain only when isinstance(response_id, str) and response_id.startswith("resp_"), otherwise keep walking back.
  3. Refuse Router groups that don't share a response store. Two deployments with different api_base, two Azure regions, or OpenAI plus Azure all pass the Responses check in _route_to_responses_api (litellm_router.py:295), and a chained call can land on a deployment that never stored the response: 12 to 15 of 20 chained calls in an offline repro. litellm's responses_api_deployment_check can't help, since the bridge returns the decoded id. Raise ValueError before any request. _group_endpoint with _reasoning_endpoint already answers "one endpoint and set of credentials?", but evaluate it on params without previous_response_id: that key isn't in _NON_ROUTING_PARAMS, so with it present every call reads as unknown reach and even a single deployment would be refused.

Also needed

  1. Refusals name the wrong flag. With only use_previous_response_id set, errors read use_responses_api=True, but ... (litellm.py:1610, litellm_router.py:307, 315, 323, 336). Name the flag the caller set and a fix that keeps chaining; the Router's "without it, ... still reaches the Responses API" hint means giving chaining up.

  2. The flag per call, via bind() or in model_kwargs is not consumed. It is never popped, so it lands in the Chat Completions request body and nothing is trimmed. Pop it in _merge_call_params and raise, saying to set it on the model (ChatOpenAI reads the field only). Restore the deleted sentence that use_responses_api is read from the call, model_kwargs or the field, since it's still true, and scope the new sentence (litellm.py:1636) to the field.

  3. A caller-supplied previous_response_id doesn't match the trim. A per-call id is sent while the history is trimmed at the history's id, and a model_kwargs id loses to the history's. Pick one rule for all sources: a caller id turns the trim off, or the history's id always wins.

  4. Refuse fallbacks when chaining on ChatLiteLLM. With fallbacks in model_kwargs, the fallback model gets only the trimmed turn plus a previous_response_id it can't use. The Router already refuses this through _unknown_reach.

  5. Set the stream id once. Drop the four setdefault("id", ...) blocks and their comment (litellm.py:2002, 2106, litellm_router.py:583, 696). litellm keeps one id per stream, so the comment doesn't hold, and after a Router mid-stream fallback the per-chunk ids merge into chatcmpl-PRIMARYresp_BACKUP.... Put the id on the finish_reason chunk so a fallback's id wins, record it only when truthy, and give the reply id a different name from the request's response_id.

  6. Chained replies drop their encrypted reasoning items. previous_response_id makes _reasoning_endpoint return None, so _keep_reasoning_items discards them. Leave out only the id the connector set when computing it; adding the key to _NON_ROUTING_PARAMS would start re-sending items for callers who pass their own id with full history.

  7. Add use_previous_response_id to _identifying_params next to use_responses_api (litellm.py:2384). Today an LLM cache serves a flag-off reply to a flag-on call with the same history.

  8. Docs. The field docstring says it "always routes the call through the Responses API" (litellm.py:1446); on ChatLiteLLMRouter it checks the deployments instead, so say so there as use_responses_api does. Also state: system messages are re-sent, only resp_ ids chain, chaining needs stored responses (store=False breaks it), and Router groups across stores are refused.

  9. Keep the 1.101.0 streamed-cost reason in the pyproject.toml comment beside the new one; it still holds.

  10. Tests, through serve_http, on both classes:

    • chaining on stream and astream (today, removing the trim from all four stream methods leaves the suite green);
    • payload["instructions"] on a chained turn, including ChatMessage(role="system");
    • a chatcmpl- reply after a resp_ one, an empty-string id, a history with no id, and the flag off;
    • the two-store Router refusal, plus a single-deployment group that still chains;
    • refusal messages that name use_previous_response_id;
    • a stream whose chunks carry two ids, merging to exactly one of them.

    test_stream_keeps_a_responses_api_id mocks one hand-built chunk, so it never merges anything and passes on 1.101.0. Send it through the bridge.

  11. Retitle to feat(chat_models): record the response id and chain calls with use_previous_response_id. The title becomes the changelog line, and chat model features are scoped chat_models.

@Akhil-Jasson Akhil Jasson (Akhil-Jasson) changed the title feat: support Responses API response-id chaining feat(chat_models): record the response id and chain calls with use_previous_response_id Oct 2, 2026
@Akhil-Jasson

Copy link
Copy Markdown
Author

I’ve updated the Responses API chaining implementation in both ChatLiteLLM and ChatLiteLLMRouter to address the review feedback. The changes preserve string-based system instructions across chained turns, only chain on valid resp_ IDs, and reject Router groups whose deployments may use different response stores. They also handle caller-supplied IDs and unsupported per-call flag usage consistently, prevent incompatible fallbacks, preserve encrypted reasoning items, and record the final streamed response ID without merging IDs from different deployments.
I’ve updated the identifying parameters and documentation, retained the existing LiteLLM 1.101.0 streamed-cost compatibility note, and added regression coverage for sync, async, streaming, and router behavior. I also updated the PR title to match the requested changelog format. Please take another look when you have a chance.

Akshay Dongare (Akshay-Dongare) added a commit that referenced this pull request Oct 3, 2026
…s it (#353)

Adds a **Sort imports** workflow that a maintainer runs before merging:
give it a PR number and it sorts that PR's imports with ruff (`I001`)
and commits the fix to the PR branch, forks included. Start it from the
Actions tab or with `gh workflow run sort-imports.yml -f pr=<number>`.

The built-in Actions token cannot push to a fork, and every outside
contribution in the last 100 PRs came from one, so the push uses a
project-owned GitHub App. A third-party app like autofix.ci would hold a
write token for every branch except `main`, and triggering on every PR
would let a stranger start a job holding one. Here only someone with
write access can start the job, and the App key lives in an environment
only `main` can use.

The job reads the PR's head commit as plain files and runs `main`'s
locked ruff over them, so no PR code executes. The token is minted only
when there is a fix, with Contents write alone, and a plain push means a
branch that moved meanwhile rejects it. Closed PRs, deleted forks and
forks without maintainer edits are refused.

**One-time setup before the first run:**
1. Create a GitHub App with repository permission Contents: Read and
write and no webhook, and install it on this repository only.
2. Create a `sort-imports` environment limited to `main`, with the App's
client ID as variable `SORT_IMPORTS_CLIENT_ID` and a private key as
secret `SORT_IMPORTS_PRIVATE_KEY`.

**Verified.** A dispatch workflow only runs from `main`, so each step's
script ran locally, verbatim from the YAML:
- the lookup resolves fork PR #349's head, refuses closed, deleted-fork
and edits-off PRs, and keeps a branch name full of shell metacharacters
as data
- an unsorted import committed on #349's head comes back byte-identical;
a clean head is left alone
- a dry-run push fast-forwards onto this branch as the bot, and is
rejected once the branch has moved
- actionlint and zizmor, auditor persona with online audits, report
nothing

An App installed only upstream can push to fork branches: autofix.ci's
does on mermaid-js/mermaid#8297 and mermaid-js/mermaid#8276.
@Akhil-Jasson

Copy link
Copy Markdown
Author

Hi Akshay Dongare (@Akshay-Dongare), could you go through the PR whenever possible and let me know if there are any other changes to be made?

This branch has not been deployed

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

Labels

feat New feature or capability

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Surface the Responses API response id and support previous_response_id chaining

2 participants