mirror of
https://github.com/zyycn/codex-proxy-rs.git
synced 2026-11-06 08:06:19 +00:00
fix(openai): align context and catalog tolerance
Co-authored-by: Codex <noreply@openai.com>
This commit is contained in:
@@ -165,6 +165,9 @@ pub enum CodexCatalogVisibility {
|
||||
Hide,
|
||||
/// 不进入 picker。
|
||||
None,
|
||||
/// 上游新增的可见性值;保留模型条目但不作本地可见性判断。
|
||||
#[serde(other)]
|
||||
Unknown,
|
||||
}
|
||||
|
||||
/// 一个已完整校验的 Codex 真实模型。
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
//! 核心 Generate operation 到 Codex Responses wire request 的严格编码。
|
||||
|
||||
use gateway_core::operation::GenerateRequest;
|
||||
use gateway_protocol::openai::WS_REQUEST_HEADER_RESPONSES_LITE_CLIENT_METADATA_KEY;
|
||||
use serde_json::{Map, Value};
|
||||
use sha2::{Digest, Sha256};
|
||||
|
||||
@@ -127,64 +128,37 @@ struct ExtractedRequestContext {
|
||||
impl ExtractedRequestContext {
|
||||
fn from_body(body: &Map<String, Value>) -> Self {
|
||||
Self {
|
||||
turn_state: body_or_metadata_string(
|
||||
body,
|
||||
&["turnState", "turn_state", "x-codex-turn-state"],
|
||||
),
|
||||
turn_metadata: body_or_metadata_string(
|
||||
body,
|
||||
&["turnMetadata", "turn_metadata", "x-codex-turn-metadata"],
|
||||
),
|
||||
beta_features: body_or_metadata_string(body, &["betaFeatures", "beta_features"]),
|
||||
version: body_or_metadata_string(body, &["version"]),
|
||||
include_timing_metrics: body_or_metadata_string(
|
||||
body,
|
||||
&["includeTimingMetrics", "include_timing_metrics"],
|
||||
),
|
||||
codex_window_id: body_or_metadata_string(
|
||||
body,
|
||||
&["codexWindowId", "codex_window_id", "x-codex-window-id"],
|
||||
),
|
||||
parent_thread_id: body_or_metadata_string(
|
||||
body,
|
||||
&[
|
||||
"parentThreadId",
|
||||
"parent_thread_id",
|
||||
"x-codex-parent-thread-id",
|
||||
],
|
||||
),
|
||||
conversation_id: body_or_metadata_string(body, &["conversation_id", "conversationId"]),
|
||||
session_id: body_or_metadata_string(body, &["session_id", "sessionId"]),
|
||||
thread_id: body_or_metadata_string(body, &["thread_id", "threadId"]),
|
||||
client_request_id: body_or_metadata_string(
|
||||
body,
|
||||
&[
|
||||
"x-client-request-id",
|
||||
"client_request_id",
|
||||
"clientRequestId",
|
||||
],
|
||||
),
|
||||
turn_id: body_or_metadata_string(body, &["turn_id", "turnId", "x-codex-turn-id"]),
|
||||
responses_lite: body_or_metadata_string(
|
||||
body,
|
||||
&["ws_request_header_x_openai_internal_codex_responses_lite"],
|
||||
),
|
||||
memgen_request: body_or_metadata_string(
|
||||
body,
|
||||
&["x-openai-memgen-request", "memgen_request"],
|
||||
),
|
||||
turn_state: body_string(body, "turnState"),
|
||||
turn_metadata: body_string(body, "turnMetadata"),
|
||||
beta_features: body_string(body, "betaFeatures"),
|
||||
version: body_string(body, "version"),
|
||||
include_timing_metrics: body_string(body, "includeTimingMetrics"),
|
||||
codex_window_id: body_string(body, "codexWindowId"),
|
||||
parent_thread_id: body_string(body, "parentThreadId"),
|
||||
conversation_id: body_string(body, "conversation_id"),
|
||||
session_id: body_string(body, "session_id"),
|
||||
thread_id: body_string(body, "thread_id"),
|
||||
client_request_id: body_string(body, "x-client-request-id"),
|
||||
turn_id: body_string(body, "turn_id"),
|
||||
// Responses Lite 在 WebSocket body 中的这个键是官方 header 投影,
|
||||
// 不是普通上下文字段的 metadata 回退。
|
||||
responses_lite: body
|
||||
.get("client_metadata")
|
||||
.and_then(Value::as_object)
|
||||
.and_then(|metadata| {
|
||||
non_empty_string(
|
||||
metadata.get(WS_REQUEST_HEADER_RESPONSES_LITE_CLIENT_METADATA_KEY),
|
||||
)
|
||||
}),
|
||||
// Memory consolidation 只由官方请求头提供;body 中同名字段不参与
|
||||
// transport 事实提取。
|
||||
memgen_request: None,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn body_or_metadata_string(body: &Map<String, Value>, keys: &[&str]) -> Option<String> {
|
||||
keys.iter()
|
||||
.find_map(|key| non_empty_string(body.get(*key)))
|
||||
.or_else(|| {
|
||||
let metadata = body.get("client_metadata")?.as_object()?;
|
||||
keys.iter()
|
||||
.find_map(|key| non_empty_string(metadata.get(*key)))
|
||||
})
|
||||
fn body_string(body: &Map<String, Value>, key: &str) -> Option<String> {
|
||||
non_empty_string(body.get(key))
|
||||
}
|
||||
|
||||
fn non_empty_string(value: Option<&Value>) -> Option<String> {
|
||||
@@ -528,10 +502,12 @@ fn apply_protocol_context(request: &mut CodexResponsesRequest, context: &Map<Str
|
||||
.parent_thread_id
|
||||
.take()
|
||||
.or_else(|| context_string(context, "parent_thread_id"));
|
||||
let prompt_cache_key = request.prompt_cache_key().map(ToOwned::to_owned);
|
||||
request.client_conversation_id = request
|
||||
.client_conversation_id
|
||||
.take()
|
||||
.or_else(|| context_string(context, "conversation_id"));
|
||||
.or_else(|| context_string(context, "conversation_id"))
|
||||
.or(prompt_cache_key);
|
||||
request.client_session_id = request
|
||||
.client_session_id
|
||||
.take()
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
use chrono::{TimeZone, Utc};
|
||||
use provider_openai::transport::profile::{CodexWireProfile, CodexWireProfileState};
|
||||
use provider_openai::transport::{
|
||||
CodexBackendClient, CodexCatalogCapabilityEvidence, CodexClientError, CodexModelCatalogError,
|
||||
CodexRequestContext, MAX_CODEX_MODEL_CATALOG_BYTES, build_reqwest_client,
|
||||
parse_codex_model_catalog,
|
||||
CodexBackendClient, CodexCatalogCapabilityEvidence, CodexCatalogVisibility, CodexClientError,
|
||||
CodexModelCatalogError, CodexRequestContext, MAX_CODEX_MODEL_CATALOG_BYTES,
|
||||
build_reqwest_client, parse_codex_model_catalog,
|
||||
};
|
||||
use wiremock::matchers::{header, method, path, query_param};
|
||||
use wiremock::{Mock, MockServer, ResponseTemplate};
|
||||
@@ -172,6 +172,20 @@ fn unknown_input_modality_should_degrade_capability_evidence_to_unknown() {
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn unknown_visibility_should_degrade_without_rejecting_the_snapshot() {
|
||||
let snapshot = parse_codex_model_catalog(
|
||||
br#"{"models":[{"slug":"gpt-future","display_name":"Future","visibility":"preview_only"}]}"#,
|
||||
None,
|
||||
)
|
||||
.expect("future visibility should not reject the snapshot");
|
||||
|
||||
assert_eq!(
|
||||
snapshot.models()[0].metadata().visibility(),
|
||||
Some(CodexCatalogVisibility::Unknown)
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn invalid_request_slug_should_fail_the_entire_snapshot() {
|
||||
let result = parse_codex_model_catalog(
|
||||
|
||||
@@ -126,6 +126,87 @@ fn encoder_should_forward_the_client_prompt_cache_key() {
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn encoder_should_restore_conversation_fallback_after_body_and_context_values() {
|
||||
let request = request(Map::from_iter([
|
||||
("model".to_owned(), json!("client-model")),
|
||||
("input".to_owned(), json!("prompt")),
|
||||
("prompt_cache_key".to_owned(), json!("cache-conversation")),
|
||||
]));
|
||||
|
||||
let encoded = encode_generate_request(&request, "gpt-test").expect("encode");
|
||||
|
||||
assert_eq!(
|
||||
encoded.client_conversation_id.as_deref(),
|
||||
Some("cache-conversation")
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn encoder_should_ignore_legacy_context_aliases_and_metadata_fallbacks() {
|
||||
let request = request(Map::from_iter([
|
||||
("model".to_owned(), json!("client-model")),
|
||||
("input".to_owned(), json!("prompt")),
|
||||
("turn_state".to_owned(), json!("legacy-turn-state")),
|
||||
(
|
||||
"x-codex-turn-state".to_owned(),
|
||||
json!("legacy-header-turn-state"),
|
||||
),
|
||||
("turn_metadata".to_owned(), json!("legacy-turn-metadata")),
|
||||
("beta_features".to_owned(), json!("legacy-beta")),
|
||||
("include_timing_metrics".to_owned(), json!("legacy-timing")),
|
||||
("codex_window_id".to_owned(), json!("legacy-window")),
|
||||
(
|
||||
"x-codex-window-id".to_owned(),
|
||||
json!("legacy-header-window"),
|
||||
),
|
||||
("parent_thread_id".to_owned(), json!("legacy-parent")),
|
||||
(
|
||||
"x-codex-parent-thread-id".to_owned(),
|
||||
json!("legacy-header-parent"),
|
||||
),
|
||||
("conversationId".to_owned(), json!("legacy-conversation")),
|
||||
("sessionId".to_owned(), json!("legacy-session")),
|
||||
("threadId".to_owned(), json!("legacy-thread")),
|
||||
("client_request_id".to_owned(), json!("legacy-request")),
|
||||
("clientRequestId".to_owned(), json!("legacy-camel-request")),
|
||||
("turnId".to_owned(), json!("legacy-turn")),
|
||||
("x-codex-turn-id".to_owned(), json!("legacy-header-turn")),
|
||||
(
|
||||
"client_metadata".to_owned(),
|
||||
json!({
|
||||
"turnState": "metadata-turn-state",
|
||||
"turnMetadata": "metadata-turn-metadata",
|
||||
"conversation_id": "metadata-conversation",
|
||||
"session_id": "metadata-session",
|
||||
"thread_id": "metadata-thread",
|
||||
"x-codex-window-id": "metadata-window"
|
||||
}),
|
||||
),
|
||||
]));
|
||||
|
||||
let encoded = encode_generate_request(&request, "gpt-test").expect("encode");
|
||||
|
||||
assert_eq!(
|
||||
(
|
||||
encoded.turn_state,
|
||||
encoded.turn_metadata,
|
||||
encoded.beta_features,
|
||||
encoded.include_timing_metrics,
|
||||
encoded.codex_window_id,
|
||||
encoded.parent_thread_id,
|
||||
encoded.client_conversation_id,
|
||||
encoded.client_session_id,
|
||||
encoded.client_thread_id,
|
||||
encoded.client_request_id,
|
||||
encoded.client_turn_id,
|
||||
),
|
||||
(
|
||||
None, None, None, None, None, None, None, None, None, None, None,
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn encoder_should_project_explicit_websocket_transport_without_touching_body() {
|
||||
let payload = ProtocolPayload::json_object(
|
||||
|
||||
Reference in New Issue
Block a user