fix(ws): harden Responses continuation state

This commit is contained in:
ZheFox
2026-08-20 08:51:16 +08:00
parent bef282cfee
commit 654f798d25
45 changed files with 6403 additions and 493 deletions
@@ -50,6 +50,69 @@ pub(crate) fn build_local_openai_responses_request_body_with_codex_model_capabil
request_headers: &http::HeaderMap,
model_capabilities: Option<&crate::ai_serving::CodexResponsesModelCapabilities>,
enable_model_directives: bool,
) -> Option<Value> {
build_local_openai_responses_request_body_with_codex_model_capabilities_and_websocket_mode(
body_json,
mapped_model,
require_streaming,
force_body_stream_field,
provider_type,
provider_api_format,
body_rules,
request_headers,
model_capabilities,
enable_model_directives,
false,
)
}
/// Builds a Responses body for a pinned WebSocket continuation.
///
/// This is intentionally an additive variant of the ordinary HTTP builder.
/// The WebSocket framing layer, rather than a JSON-body heuristic, tells the
/// Codex compatibility pass that `previous_response_id` is transport state and
/// that Responses Lite `tools`/`instructions` must not be materialized into a
/// second historical input prefix.
pub(crate) fn build_local_openai_responses_request_body_with_codex_model_capabilities_for_websocket_continuation(
body_json: &Value,
mapped_model: &str,
require_streaming: bool,
force_body_stream_field: bool,
provider_type: &str,
provider_api_format: &str,
body_rules: Option<&Value>,
request_headers: &http::HeaderMap,
model_capabilities: Option<&crate::ai_serving::CodexResponsesModelCapabilities>,
enable_model_directives: bool,
) -> Option<Value> {
build_local_openai_responses_request_body_with_codex_model_capabilities_and_websocket_mode(
body_json,
mapped_model,
require_streaming,
force_body_stream_field,
provider_type,
provider_api_format,
body_rules,
request_headers,
model_capabilities,
enable_model_directives,
true,
)
}
#[allow(clippy::too_many_arguments)]
fn build_local_openai_responses_request_body_with_codex_model_capabilities_and_websocket_mode(
body_json: &Value,
mapped_model: &str,
require_streaming: bool,
force_body_stream_field: bool,
provider_type: &str,
provider_api_format: &str,
body_rules: Option<&Value>,
request_headers: &http::HeaderMap,
model_capabilities: Option<&crate::ai_serving::CodexResponsesModelCapabilities>,
enable_model_directives: bool,
websocket_continuation: bool,
) -> Option<Value> {
let provider_request_body = surface_build_local_openai_responses_request_body(
body_json,
@@ -68,15 +131,27 @@ pub(crate) fn build_local_openai_responses_request_body_with_codex_model_capabil
.get("model")
.and_then(Value::as_str)
.unwrap_or(mapped_model);
crate::ai_serving::apply_codex_openai_responses_special_body_edits_with_source_model_and_capabilities(
&mut provider_request_body,
provider_type,
provider_api_format,
mapped_model,
source_model,
model_capabilities,
body_rules,
);
if websocket_continuation {
crate::ai_serving::apply_codex_openai_responses_websocket_continuation_body_edits_with_source_model_and_capabilities(
&mut provider_request_body,
provider_type,
provider_api_format,
mapped_model,
source_model,
model_capabilities,
body_rules,
);
} else {
crate::ai_serving::apply_codex_openai_responses_special_body_edits_with_source_model_and_capabilities(
&mut provider_request_body,
provider_type,
provider_api_format,
mapped_model,
source_model,
model_capabilities,
body_rules,
);
}
apply_openai_responses_compact_special_body_edits(
&mut provider_request_body,
provider_api_format,
@@ -153,7 +153,7 @@ fn local_openai_responses_wrapper_preserves_body_order_after_edits() {
}
#[test]
fn local_openai_responses_wrapper_strips_foreign_reasoning_item_ids() {
fn local_openai_responses_wrapper_defers_reasoning_replay_filtering() {
let body_json = json!({
"model": "gpt-5.4",
"input": [
@@ -184,9 +184,14 @@ fn local_openai_responses_wrapper_strips_foreign_reasoning_item_ids() {
let input = provider_request_body["input"]
.as_array()
.expect("input array");
assert_eq!(input.len(), 2);
// This provider-agnostic normalization layer cannot decide whether an
// id-less/foreign reasoning item is opaque state required by DeepSeek.
// The provider-aware finalization pass applies the strict or DeepSeek
// replay policy once the selected upstream base URL is known.
assert_eq!(input.len(), 3);
assert_eq!(input[0]["id"], "rs_provider_123");
assert_eq!(input[1]["type"], "message");
assert_eq!(input[1]["id"], "item_72d3bd8d367d01977ace23f1");
assert_eq!(input[2]["type"], "message");
}
#[test]