Skip to content

fix(validate): reject conflicting history selectors - #999

Open
derekhiggins wants to merge 1 commit into
praxis-proxy:mainfrom
derekhiggins:fix/validate-conflicting-history-selectors
Open

fix(validate): reject conflicting history selectors#999
derekhiggins wants to merge 1 commit into
praxis-proxy:mainfrom
derekhiggins:fix/validate-conflicting-history-selectors

Conversation

@derekhiggins

Copy link
Copy Markdown

Reject Responses API requests that provide both previous_response_id and conversation instead of silently prioritizing one history source. Return the OpenAI-compatible invalid-request JSON error for both streaming and non-streaming requests.

Fixes: #840

.get_metadata("openai_responses_format.stream")
.is_some_and(|value| value == "true");
reject_invalid(
"Mutually exclusive parameters. Ensure you are only providing one of: 'previous_response_id' or 'conversation'.",

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.

we should return something similar to openai i guess:

{
"message": "Mutually exclusive parameters: ''. Ensure you are only providing one of: 'pre..._id' or 'conversation'.",
"type": "invalid_request_error",
"param": null,
"code": "mutually_exclusive_parameters"
}

i don't mind about the message but the code is incorrect here

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

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

I can fix this, but I think we have the same (pre existing problem) on many error responses,
when developing this all of the errors messages I looked at used responses_error_body (or responses_error_sse_payload), this takes a single code param and then uses it for both code and type in the error response.

pub(crate) fn responses_error_body(code: &str, message: &str) -> Bytes {
Bytes::from(
serde_json::json!({
"error": {
"message": message,
"type": code,
"param": null,
"code": code,
},

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.

Let's fix it here and have another PR for the rest of the code, how does that sound?

@leseb

leseb commented Sep 8, 2026

Copy link
Copy Markdown
Collaborator

@derekhiggins also please add a test in test_openai_responses_vllm.py to validate the error shape with the client, thanks (streaming and buffered mode).

Reject Responses API requests that provide both previous_response_id and
conversation instead of silently prioritizing one history source.
Return an OpenAI-compatible invalid-request error in JSON or SSE as
appropriate, and validate both forms through the official OpenAI client.

Fixes: praxis-proxy#840

Signed-off-by: Derek Higgins <derekh@redhat.com>
@derekhiggins
derekhiggins force-pushed the fix/validate-conflicting-history-selectors branch from dc2af51 to a335a94 Compare September 8, 2026 16:23

@praxis-bot praxis-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.

Clean, well-layered change. Moving the mutual-exclusion check into the validate filter is the right call -- it catches the conflict before generating IDs, enriching context, or touching the store. The null-awareness in reject_conflicting_history_selectors correctly handles previous_response_id: null (OpenAI SDK sends this for absent fields). Test coverage is thorough: unit tests for both buffered and streaming rejection, integration test with a real conversation + nonexistent response ID to prove the rejection fires before any store lookup, and SDK-level tests that verify the exact error shape the OpenAI client parses.

One documentation gap noted below.

@@ -100,9 +100,6 @@ impl RehydrateFilter {
/// Parse body, resolve rehydration source (`previous_response_id` or

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

[Medium] The precedence doc comment is removed, but the rehydrate method still resolves previous_response_id before conversation (lines 115-117). Since the upstream validate filter now rejects conflicts, this code path is unreachable in the standard pipeline -- but it remains as an implicit defense-in-depth fallback.

Add a brief doc note so future maintainers know this ordering exists intentionally and that the validate filter is the expected enforcement point:

/// Parse body, resolve rehydration source (`previous_response_id` or
/// `conversation`), and populate [`ResponsesState`] with the full
/// conversation history.
///
/// The upstream `openai_responses_validate` filter rejects requests
/// that supply both selectors; the resolution order here is a
/// silent fallback only.

@leseb leseb assigned derekhiggins and unassigned leseb Sep 9, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

Development

Successfully merging this pull request may close these issues.

Responses rehydration silently chooses previous_response_id when conversation is also set

3 participants