Skip to content

feat: add DELETE /v1/responses/{id} and GET /v1/responses/{id}/input_items - #466

Merged
haoshan98 merged 7 commits into
vllm-project:mainfrom
ENCHIGO:feat/responses-delete-and-input-items
Oct 26, 2026
Merged

haoshan98 merged 7 commits into
vllm-project:mainfrom
ENCHIGO:feat/responses-delete-and-input-items

Conversation

@ENCHIGO

@ENCHIGO ENCHIGO commented Oct 19, 2026

Copy link
Copy Markdown
Contributor

Summary

Adds the two stored-response routes that #462 reports missing. Both act on local storage and use the same authentication layer as GET /v1/responses/{response_id}.

DELETE /v1/responses/{response_id} returns {"id": ..., "object": "response", "deleted": true}.

  • Only the responses row is removed. The schema already provides for this: responses.previous_response_id and conversations.latest_response_id are both ON DELETE SET NULL.
  • Responses created from the deleted one stay retrievable and can still be continued. persist_with_conversation_id copies the parent's history_item_ids into every child, so a child never reads its parent row.
  • A new request that names the deleted ID as previous_response_id gets 404, and no inference runs.
  • Items are deliberately left in place. A response-history item ID is copied into the history of every descendant, so it is not owned by one response. Proving an item is unreferenced would need a scan of every history_item_ids array, so reclamation is out of scope here (see the questions on Add DELETE /v1/responses/{id} and GET /v1/responses/{id}/input_items #462).

GET /v1/responses/{response_id}/input_items lists the context the response was generated from: items inherited through previous_response_id or its conversation, including earlier turns' outputs, followed by this turn's input. The response's own output is excluded, identified by the output item IDs in its stored snapshot.

  • I chose this reading over "this turn's input only" because the OpenAI SDK types the listing as ResponseItem, which covers output items as well as inputs. If you'd rather list this turn's input only, that needs a boundary recorded at persist time, and I'm happy to switch.
  • It reads the stored history_item_ids snapshot, which both the previous_response_id and conversation paths record. A conversation turn therefore lists the conversation as it was at that turn, and deleting a parent does not break a child's listing.
  • Pagination, validation and errors follow GET /v1/conversations/{id}/items:
    • limit 1..=100, default 20
    • order defaults to desc
    • an unknown after returns 404 naming after
  • The shared query validation moved onto ListItemsQuery so both listings use the same checks.
  • Records without a stored snapshot return 409, as retrieval does.

README.md and docs/api/index.md document both routes.

Closes #462

Test Plan

  • New tests/response_input_items_test.rs:
    • a previous_response_id chain lists first, answer 0, second and excludes its own msg_answer_1; the default desc order is reversed; walking limit=1 pages with after reproduces the same sequence
    • a conversation's earlier turn does not see later turns
    • 401 without a key; 404 for unstored and unknown IDs; 400 for a bad limit, order or include; 404 with param: after for an unknown cursor; 409 for a legacy record; 404 after DELETE
  • tests/response_retrieval_test.rs: deleting a parent keeps the child retrievable and lets it be continued with full history (the upstream receives first, answer, second, answer, third); continuing from the deleted ID returns 404 with no inference; a repeat delete returns 404; 401 without a key.
  • Mutation check: with the own-output exclusion disabled, both listing scenarios fail.
  • cargo test --workspace (all 106 test binaries pass), cargo clippy --all-targets -- -D warnings, cargo fmt -- --check, and pre-commit run --all-files are clean.

@chatgpt-codex-connector

chatgpt-codex-connector Bot commented Oct 19, 2026 •

Copy link
Copy Markdown

Codex Review Summary

This comment shows the latest Codex review activity on this pull request.

Review Status Commit Review trigger
📝 Code Review ✅ Completed 2026-10-19T17:42:57.883735Z b9a8429 PR opened
ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review" or "@codex security review".

Codex reacts with 👀 while any review is running, comments if it has suggestions, and reacts with 👍 once all reviews finish with no findings.

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: b9a8429d53

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

@maralbahari maralbahari left a comment

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 for contributing! I’ve added inline comments with a few concerns and suggestions. Like the Conversations API, these endpoints need cassette recordings to verify OpenAI behavior and gateway parity. If you need help recording the cassettes, please let me know.

Some(cursor) => {
items
.iter()
.position(|item| item.id == cursor)

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.

The cursor lookup uses .position() on the public item ID, which always selects the first occurrence. Public IDs can occur more than once in conversation history.

I reproduced this by creating a conversation containing two occurrences of the same public item ID and generating a response from it. With order=asc&limit=1, the next page returns the same ID with has_more: true. Subsequent requests using that last_id repeat the same page indefinitely.

Please establish OpenAI's cursor behavior for repeated IDs through a reference recording, then implement that behavior and add a regression test proving pagination makes forward progress. The current test only covers unique IDs.

Like the Conversations API, this feature requires OpenAI and gateway cassette recordings to verify behavior. See the Conversations edge-case YAML and the corresponding record_conversations_api_cassettes.sh as examples.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Code fixed in a31b6d2. A cursor naming a repeated public ID now resumes after its furthest occurrence in the requested order, the same rule item::list_for_conversation applies, so paging always advances. Regression: input_item_pages_make_progress_past_a_repeated_public_id walks both orders with limit=1 over a history containing msg_reused twice. Reverting to .position() makes it fail. Consequence of matching conversations: items between two occurrences are skipped by that cursor. I don't have OpenAI API access to record the reference behavior, so I'm leaving this thread open until someone with access can record it. I can prepare the recorder scenario if that helps.

pub fn response(id: String) -> Self {
Self {
id,
object: "response".to_string(),

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.

The new constructor returns "object": "response", while OpenAI's current DELETE reference specifies "object": "response.deleted".

The integration test and API documentation also assert the current implementation's value, so passing those tests does not establish compatibility.

Please verify the actual response through an OpenAI recording and align the constructor, test assertions, and documentation with the verified contract.

Like the Conversations API, this feature requires OpenAI and gateway cassette recordings to verify behavior. See the Conversations deletion YAML and the corresponding record_conversations_api_cassettes.sh as examples.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Changed in a31b6d2 to "object": "response.deleted", together with the test assertion and docs/api/index.md. I verified it against OpenAI's published OpenAPI definition (https://github.com/openai/openai-openapi/blob/master/openapi.yaml): deleteResponse declares object as the const response.deleted, and the example body is {"id": "resp_123", "object": "response.deleted", "deleted": true}. That is the spec, not a live recording. I don't have OpenAI API access to record one, so I'm leaving this open for a cassette.

pub async fn list_input_items(
State(state): State<AppState>,
Path(response_id): Path<String>,
axum::extract::Query(query): axum::extract::Query<super::conversation_items::ListItemsQuery>,

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.

OpenAI defines include as an array and documents values beyond message.output_text.logprobs. This route reuses an Option<String> query field and validation that only accepts that one expansion.

I reproduced both failures:

  • include[]=message.input_image.image_url returns 400 with "unsupported include value".
  • Multiple include[] parameters return 400 with the plain-text body Failed to deserialize query string: duplicate field `include` .

Please look at ListItemsQuery in conversation_items.rs: it already defines include and is reused by this handler. Extend that shared query type to represent supported include values with an enum and accept a collection of them, with structured API errors for malformed queries. At this revision, include is an Option<String> field on a struct, not an enum.

Like the Conversations API, this feature requires OpenAI and gateway cassette recordings to verify behavior. See the Conversations pagination YAML and the corresponding record_conversations_api_cassettes.sh as examples.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Changed in a31b6d2. ListItemsQuery is parsed from the raw query into typed fields: include: Vec<ItemInclude>, repeatable and accepted as both include and include[]. ItemInclude has the 8 values of OpenAI's IncludeEnum (https://github.com/openai/openai-openapi/blob/master/openapi.yaml). The values stored items already carry are accepted: message.input_image.image_url, computer_call_output.output.image_url, reasoning.encrypted_content and message.output_text.logprobs. Hosted tool results and unknown values return a structured invalid_request 400. A repeated limit/order/after also returns a structured 400 instead of the plain-text deserializer error. Conversation item listing uses the same parser. Tests cover a repeated include[] with image_url, and the 400 bodies. Which hosted-tool expansions should be servable is open to discussion, and recordings are pending as noted on the other threads.

}

/// Like [`answering_upstream`], but each turn's output starts with `extra_output`.
fn answering_upstream_with(extra_output: Value) -> Router {

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.

The PR adds mock-based tests but no OpenAI reference cassettes. Its description explicitly derives the inherited-history behavior from the SDK's ResponseItem union. That union describes permissible item shapes; it does not establish which historical occurrences this endpoint returns.

Like the Conversations API, this feature requires OpenAI and gateway cassette recordings to verify history, pagination, include parameters, and deletion behavior, including cached WebSocket continuations. Mock-based tests alone do not establish parity.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Agreed that mock tests don't establish parity. I don't have OpenAI API access, so I can't produce the OpenAI side of these cassettes. This revision checks the DELETE shape and the include values against OpenAI's published OpenAPI definition, and it adds regressions for the gateway-side behavior: repeated-ID pagination, include arrays, and structured query errors. History contents, cursor semantics for repeated IDs, and WebSocket continuations after deletion still need a reference recording. If a maintainer can run the recorder, I can add the scenarios to a recorder script modeled on record_conversations_api_cassettes.sh for that. I'm leaving this open until then.

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.

@ENCHIGO I will help you to add those yamls and test your branch.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Thank you, @maralbahari! The branch is rebased on current main at f5b8d7e. If it helps the recorder, the cases worth capturing are: DELETE body and a continuation after deletion (400 previous_response_not_found), input_items for a previous_response_id chain and for a conversation turn, a history where one public ID repeats with limit=1 paging, and the include values.

@ENCHIGO
ENCHIGO force-pushed the feat/responses-delete-and-input-items branch from aeea461 to a8f9e38 Compare October 20, 2026 11:25
@ENCHIGO

ENCHIGO commented Oct 20, 2026

Copy link
Copy Markdown
Contributor Author

Rebased onto main (62471db) to resolve the conflicts with #465 and #455.

  • refactor: unify request and conversation history items and simplify compaction ownership #465: ConversationItem was removed, so input_items now converts rows with InOutItem::try_from, the same way conversation item listing does.
  • fix: give each response-history item occurrence its own row id #455: each history occurrence now has its own row ID, and a public ID can repeat. Matching this turn's output by public ID would also drop an earlier turn's output that shares the ID. So persistence now records the output row IDs after insertion, and input_items matches on those. Records written before this change fall back to the snapshot's public IDs.
  • New test input_items_keep_an_earlier_turn_output_that_shares_this_turn_output_id: the upstream returns the same message ID on two turns. The test fails if public-ID matching is restored.

cargo test --workspace (110 test binaries), workspace clippy with -D warnings, fmt and pre-commit run --all-files all pass. The new helper lives in storage/response.rs so that item.rs stays within its file-size budget.

@ENCHIGO
ENCHIGO force-pushed the feat/responses-delete-and-input-items branch from a8f9e38 to a31b6d2 Compare October 21, 2026 18:15
@ENCHIGO
ENCHIGO force-pushed the feat/responses-delete-and-input-items branch from a31b6d2 to f5b8d7e Compare October 22, 2026 08:01
Stored responses could be retrieved but not deleted (405), unlike
Conversations, which already support deletion.

Deleting removes the response row only. A response-history item id is
copied into the history_item_ids of every descendant, so items are not
owned by a single response and stay in place. The existing foreign keys
clear responses.previous_response_id and conversations.latest_response_id
references to the deleted row, so descendants stay retrievable and can
still be continued with their full history, while a new request naming the
deleted id as previous_response_id is rejected as not found.

The route shares retrieval's credential check because both act on local
storage. The response body matches the OpenAI deletion object:
{"id": ..., "object": "response", "deleted": true}.

Refs vllm-project#462

Signed-off-by: fengjiaqi <enchiigo@gmail.com>
Lists the context a stored response was generated from: items inherited
through previous_response_id or its conversation, including earlier turns'
outputs, followed by that turn's input. The response's own output is
excluded, identified by the output item IDs in its stored snapshot. This
matches the OpenAI listing, whose item type also covers prior outputs.

Every stored response already records its full history_item_ids, on both
the previous_response_id and the conversation path, so the listing reads
that snapshot rather than the live conversation: a turn lists the
conversation as it was at that turn, and a deleted parent does not break a
child's listing.

Pagination, validation and errors follow conversation item listing: limit
1..=100 (default 20), order desc by default, an unknown after cursor is a
404 naming `after`. The shared query validation moves onto ListItemsQuery
so both listings use it. Records without a snapshot return 409, as
retrieval does. Authentication matches retrieval.

Closes vllm-project#462

Signed-off-by: fengjiaqi <enchiigo@gmail.com>
input_items excluded a response's own output by the wire IDs in its
snapshot. Output kinds whose wire id is optional (compaction, shell_call)
get a generated ID when stored, so they were missing from that set and
were listed as input. And since vllm-project#455 a public ID can recur in history,
so matching by public ID would also drop an earlier turn's output that
shares this turn's ID.

Persistence now records the row IDs of the rows it inserts as output in
ResponseMetadata.output_item_ids, on both the previous_response_id and
conversation paths, and input_items matches rows by row ID. Records
written before this fall back to the snapshot's public IDs. Regression
tests cover an ID-less compaction and an upstream reusing one message ID
across turns; each fails when its half of the fix is removed.

Deleting a response does not invalidate a WebSocket session that caches it
as its latest checkpoint. Checking storage before reusing a cached durable
parent was tried and reverted: the extra await reordered automatic steering
successors, and the edge-input-lifecycle/old-parent-before-successor replay
failed 5 of 6 runs (6/6 on main and after the revert). Sessions are per
connection and may be on another replica, so the exception is documented
on ResponseStore::delete and in the API docs.

Signed-off-by: fengjiaqi <enchiigo@gmail.com>
- Parse item listing queries once into a typed ItemLimit, ItemOrder and a
  repeatable ItemInclude list, shared by conversation item listing and
  response input items; malformed queries return structured 400 errors.
- Resume an input_items cursor after the furthest occurrence of a repeated
  public ID, as conversation item listing does, so paging always advances.
- Return "object": "response.deleted" from DELETE /v1/responses/{id},
  per OpenAI's published OpenAPI definition.
- Serialize new items into NewItem (public ID, data, input/output role) and
  return WrittenItem rows that keep that role, deriving output row IDs
  from them instead of a parallel boolean array.
- Record output row IDs through the one sanitizing metadata conversion,
  now consuming, so persistence clones the metadata once.

Signed-off-by: fengjiaqi <enchiigo@gmail.com>
- Delete the deleted response's history rows that nothing else keeps: rows
  another response lists, rows that belong to a conversation, and rows a
  conversation item references stay.
- When the response was its conversation's latest, point the conversation
  at its newest remaining response instead of clearing it, so the next
  turn keeps the closest stored state.
- Report an unknown previous_response_id on the HTTP path as 400
  previous_response_not_found, as OpenAI does and the WebSocket path
  already did.

Signed-off-by: fengjiaqi <enchiigo@gmail.com>
Signed-off-by: maral <maralbahari.98@gmail.com>
Signed-off-by: maral <maralbahari.98@gmail.com>
@maralbahari

maralbahari commented Oct 23, 2026 •

Copy link
Copy Markdown
Collaborator

@ENCHIGOI recorded OpenAI’s behavior and replayed the cases through the gateway. The recordings exposed gaps in deletion, pagination, tool item IDs/statuses, and error responses. Some failures returned different HTTP statuses, error codes, or error metadata (type, param, and message) from OpenAI; I aligned those recorded cases.

For tool item IDs and statuses, some result types did not retain these fields, and conversions could discard them before persistence. This left /responses/{id}/input_items with incomplete tool history. I addressed this by preserving IDs and explicit statuses through existing types and conversions, generating IDs for gateway-created results, and defaulting omitted client-result statuses during normalization. Explicit failure or incomplete statuses remain intact.

The larger number of changed files comes partly from closing these gaps: preserving IDs and statuses requires updating the relevant item types, tool-result creation paths, conversions, and their callers. The additional recordings and regression tests also account for a substantial part of the changes.

I also simplified the implementation:

  • Removed the redundant new_item.rs representation and reused InOutItem.
  • Removed output_item_ids metadata, which duplicated information available through ordered history and the response snapshot.
  • Reused shared pagination types without adding another per-item wrapper.

Tests now check recorded behavior and replay requests through the gateway with mocked upstream responses. All local tests passed. The gateway tool recordings still need refreshing after the final listing changes before claiming full parity for those cases.

For future PRs, please reuse existing types, conversions, and storage operations before introducing new representations or metadata. Preserve identity and status throughout conversions, keep responsibilities in their existing layers, and verify both successful responses and error details against OpenAI recordings.

@ENCHIGO

ENCHIGO commented Oct 23, 2026

Copy link
Copy Markdown
Contributor Author

Thank you, @maralbahari, for recording the OpenAI cases and closing the gaps they exposed. Noted on reusing existing types, conversions and storage operations, and on verifying error details against recordings; I'll follow that in future PRs. I won't push to this branch further unless you ask, so your changes stay as they are.

@haoshan98
haoshan98 merged commit 42c64c7 into vllm-project:main Oct 26, 2026
17 checks passed
maralbahari pushed a commit that referenced this pull request Oct 26, 2026
## Summary

Fixes #479.

On PostgreSQL, every stored conversation turn returned 500 before
inference ran. The turn calls `conversation::get_or_create`, whose
upsert ends in `ON CONFLICT (id) DO UPDATE SET created_at = created_at`.
In that clause an unqualified `created_at` could mean the existing row
or `EXCLUDED`, so PostgreSQL rejects the statement as ambiguous
(SQLSTATE 42702). SQLite accepts it.

The update now reads `SET created_at = conversations.created_at`. It is
still a no-op whose only job is to make `RETURNING` yield the existing
row. `DO NOTHING` would not work here, because it returns no row on
conflict. This is the only `ON CONFLICT` in production code.

CI's `postgres` job already runs `postgres_storage_integration`, so the
new test runs there without any workflow change.

## Test Plan

- New
`postgres_get_or_create_creates_then_returns_the_existing_conversation`
in `postgres_storage_integration.rs`:
1. The first `get_or_create` creates the conversation, and a turn is
persisted into it.
  2. The second call returns the same conversation and `created_at`.
3. The conversation version (including `latest_response_id`) and its
item are unchanged.
- **RED** on a7dc7b5 against `postgres:17-alpine`: the first call fails
with `42702 column reference "created_at" is ambiguous`.
- **GREEN** with the fix. All three of CI's PostgreSQL steps pass
locally against `postgres:17-alpine`:
  - `storage::schema::tests::` (3);
  - `postgres_storage_integration` (6, including the new test);
  - `postgres_tenant_isolation_test` (6).
- End to end: while reviewing #466 against PostgreSQL, a `POST
/v1/responses` turn with `conversation` returned this 500. With the same
one-line change, the turn and that PR's conversation listing test
passed.
- The SQLite `test_conversation_get_or_create_same_id` still passes.
- `cargo fmt -- --check`, `cargo clippy --workspace --all-targets -- -D
warnings`, the Rust file-size ratchet and `pre-commit run --all-files`
are clean.
- `cargo test --workspace --no-fail-fast`: 111 test binaries ran, with
2719 tests passing and 1 failing. The failure is
`tool::web_fetch::http::tests::the_deadline_bounds_a_slow_origin`, a
wall-clock bound in code this PR doesn't touch. It also fails on `main`
on my machine when the whole suite runs in parallel, and it passes 3 of
3 runs in isolation.

Signed-off-by: Zheng Lu <Lz429671594@gmail.com>
haoshan98 added a commit to EmbeddedLLM/agentic-api that referenced this pull request Oct 26, 2026
Brings in vllm-project#466 (DELETE /v1/responses/{id} and GET .../input_items; tool
outputs carry ids and statuses) and vllm-project#475 (HTTP store:false runs through the
executor). Lowered search outputs and replayed gateway calls carry no id or
status; replayed search history is checked on vllm-project#466's resolved items; a
replayed gateway record resolved to its stored output stays an input record,
so store:false replays of stored MCP, web search and multi-agent hosted items
still reach the model. Tests updated for vllm-project#466's duplicate-item check and

Signed-off-by: haoshan98 <haoshanw@gmail.com>
vllm-project#475's unstored conversation turns.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Add DELETE /v1/responses/{id} and GET /v1/responses/{id}/input_items

4 participants