fix(openai): align structured location metadata (#88)

Rewrite only Codex-generated environment context and web search location fields to Piketon, Ohio. Preserve user-authored text and absolute timestamps.

Co-authored-by: Zyy <zyycn@users.noreply.github.com>
Co-authored-by: Codex <noreply@openai.com>
This commit is contained in:
Zyy
2026-09-14 14:14:08 +08:00
committed by GitHub
co-authored by Zyy Codex
parent e8e2f18927
commit d8bc91efce
4 changed files with 249 additions and 0 deletions
+1
View File
@@ -2717,6 +2717,7 @@ dependencies = [
"base64 0.23.1",
"bytes",
"chrono",
"chrono-tz",
"cookie",
"flate2",
"futures",
@@ -13,6 +13,7 @@ async-trait = "0.1.89"
base64 = "0.23.0"
bytes = "1.11.1"
chrono = { version = "0.4.45", features = ["clock", "serde"] }
chrono-tz = "0.10"
cookie = "0.18.1"
futures = "0.3.32"
flate2 = "1.1.9"
@@ -3,11 +3,14 @@
use std::io;
use base64::{Engine as _, engine::general_purpose::STANDARD};
use chrono::{DateTime, Utc};
use chrono_tz::America::New_York;
use gateway_core::operation::GenerateRequest;
use gateway_protocol::openai::{
WS_REQUEST_HEADER_RESPONSES_LITE_CLIENT_METADATA_KEY, is_transport_managed_request_header,
};
use reqwest::header::{HeaderMap, HeaderName, HeaderValue};
use roxmltree::Document;
use serde::Serialize as _;
use serde_json::{Map, Value};
use sha2::{Digest, Sha256};
@@ -20,6 +23,12 @@ const PASSTHROUGH_HEADERS_CONTEXT_KEY: &str = "opaque_request_headers";
const TURN_ID_CLIENT_METADATA_KEY: &str = "turn_id";
const THREAD_SPAWN_SUBAGENT_KIND: &str = "thread_spawn";
const THREAD_SPAWN_CONVERSATION_PREFIX: &str = "thread-spawn:";
const ENVIRONMENT_CONTEXT_CONTENT_KIND: &str = "environments.environment_context";
// 请求中的地区画像以 PORTS 所在的 Piketon 为基准;epoch 时间戳仍保持绝对时间原值。
const TARGET_COUNTRY: &str = "US";
const TARGET_REGION: &str = "Ohio";
const TARGET_CITY: &str = "Piketon";
const TARGET_TIMEZONE: &str = "America/New_York";
const UNSUPPORTED_CODEX_RESPONSES_FIELDS: &[&str] = &["max_output_tokens", "temperature"];
const CROSS_ACCOUNT_IDENTITY_KEYS: &[&str] = &[
@@ -113,6 +122,128 @@ fn adapt_codex_responses_body(body: &mut Map<String, Value>, upstream_model: &st
for field in UNSUPPORTED_CODEX_RESPONSES_FIELDS {
body.remove(*field);
}
align_structured_location_fields(body, Utc::now());
}
fn align_structured_location_fields(body: &mut Map<String, Value>, now: DateTime<Utc>) {
let current_date = now.with_timezone(&New_York).format("%Y-%m-%d").to_string();
if let Some(input) = body.get_mut("input").and_then(Value::as_array_mut) {
for item in input {
align_environment_context(item, &current_date);
}
}
if let Some(tools) = body.get_mut("tools").and_then(Value::as_array_mut) {
for tool in tools {
align_web_search_location(tool);
}
}
}
fn align_environment_context(item: &mut Value, current_date: &str) {
let Some(item) = item.as_object_mut() else {
return;
};
if item.get("role").and_then(Value::as_str) != Some("user") {
return;
}
let content_kinds = item
.get("internal_chat_message_metadata_passthrough")
.and_then(Value::as_object)
.and_then(|metadata| metadata.get("content_item_kinds"))
.and_then(Value::as_array)
.map(|kinds| {
kinds
.iter()
.map(|kind| kind.as_str().map(str::to_owned))
.collect::<Vec<_>>()
});
let Some(content) = item.get_mut("content").and_then(Value::as_array_mut) else {
return;
};
for (index, part) in content.iter_mut().enumerate() {
if content_kinds
.as_ref()
.and_then(|kinds| kinds.get(index))
.and_then(Option::as_deref)
!= Some(ENVIRONMENT_CONTEXT_CONTENT_KIND)
{
continue;
}
let Some(part) = part.as_object_mut() else {
continue;
};
if part.get("type").and_then(Value::as_str) != Some("input_text") {
continue;
}
let Some(Value::String(text)) = part.get_mut("text") else {
continue;
};
if let Some(aligned) = aligned_environment_context(text, current_date) {
*text = aligned;
}
}
}
fn aligned_environment_context(text: &str, current_date: &str) -> Option<String> {
let trimmed = text.trim();
if !trimmed.starts_with("<environment_context>") || !trimmed.ends_with("</environment_context>")
{
return None;
}
let document = Document::parse(text).ok()?;
let root = document.root_element();
if !root.has_tag_name("environment_context") {
return None;
}
let mut replacements = root
.children()
.filter(|node| node.is_element())
.filter_map(|node| {
let replacement = match node.tag_name().name() {
"current_date" => format!("<current_date>{current_date}</current_date>"),
"timezone" => format!("<timezone>{TARGET_TIMEZONE}</timezone>"),
_ => return None,
};
Some((node.range(), replacement))
})
.collect::<Vec<_>>();
if replacements.is_empty() {
return None;
}
replacements.sort_unstable_by_key(|(range, _)| std::cmp::Reverse(range.start));
let mut aligned = text.to_owned();
for (range, replacement) in replacements {
aligned.replace_range(range, &replacement);
}
Some(aligned)
}
fn align_web_search_location(tool: &mut Value) {
let Some(tool) = tool.as_object_mut() else {
return;
};
let Some(tool_type) = tool.get("type").and_then(Value::as_str) else {
return;
};
if tool_type != "web_search" && !tool_type.starts_with("web_search_") {
return;
}
tool.insert(
"user_location".to_owned(),
Value::Object(Map::from_iter([
("type".to_owned(), Value::String("approximate".to_owned())),
(
"country".to_owned(),
Value::String(TARGET_COUNTRY.to_owned()),
),
("region".to_owned(), Value::String(TARGET_REGION.to_owned())),
("city".to_owned(), Value::String(TARGET_CITY.to_owned())),
(
"timezone".to_owned(),
Value::String(TARGET_TIMEZONE.to_owned()),
),
])),
);
}
fn extract_request_context(request: &mut CodexResponsesRequest) {
@@ -1,3 +1,5 @@
use chrono::{Datelike as _, Utc};
use chrono_tz::America::New_York;
use gateway_core::operation::{GenerateRequest, ProtocolPayload};
use serde_json::{Map, Value, json};
@@ -117,6 +119,120 @@ fn encoder_should_remove_unsupported_fields_from_upstream_body() {
);
}
#[test]
fn encoder_should_align_structured_location_fields_without_rewriting_chat_text() {
let normal_chat = "<environment_context>\n <current_date>2026-09-13</current_date>\n \
<timezone>Asia/Shanghai</timezone>\n</environment_context>";
let body = json!({
"model": "client-model",
"input": [
{
"type": "message",
"role": "user",
"content": [{
"type": "input_text",
"text": "<environment_context>\n <cwd>/Users/mike/Personal/workspace/PORTAL2</cwd>\n <shell>zsh</shell>\n <current_date>2026-09-13</current_date>\n <timezone>Asia/Shanghai</timezone>\n <filesystem><file_system type=\"unrestricted\" /></filesystem>\n</environment_context>"
}],
"internal_chat_message_metadata_passthrough": {
"create_time": 1789293131.822,
"content_item_kinds": ["environments.environment_context"]
}
},
{
"type": "message",
"role": "user",
"content": [{"type": "input_text", "text": normal_chat}],
"internal_chat_message_metadata_passthrough": {
"content_item_kinds": ["user.text"]
}
},
{
"type": "message",
"role": "user",
"content": [{"type": "input_text", "text": normal_chat}]
}
],
"tools": [
{"type": "web_search"},
{
"type": "function",
"name": "remember_timezone",
"description": "Keep Asia/Shanghai unchanged"
}
],
"client_metadata": {
"x-codex-turn-metadata": "{\"turn_started_at_unix_ms\":1789293131822}",
"x-codex-ws-stream-request-start-ms": "1789293132000"
}
})
.as_object()
.expect("request object")
.clone();
let before = Utc::now().with_timezone(&New_York);
let encoded = encode_generate_request(&request(body), "gpt-test").expect("encode");
let after = Utc::now().with_timezone(&New_York);
let encoded = Value::Object(encoded.body().clone());
let environment = encoded
.pointer("/input/0/content/0/text")
.and_then(Value::as_str)
.expect("environment context text");
let expected_dates = [
format!(
"{:04}-{:02}-{:02}",
before.year(),
before.month(),
before.day()
),
format!(
"{:04}-{:02}-{:02}",
after.year(),
after.month(),
after.day()
),
];
assert!(
expected_dates
.iter()
.any(|date| environment.contains(&format!("<current_date>{date}</current_date>")))
);
assert!(environment.contains("<timezone>America/New_York</timezone>"));
assert_eq!(
encoded.pointer("/tools/0/user_location"),
Some(&json!({
"type": "approximate",
"country": "US",
"region": "Ohio",
"city": "Piketon",
"timezone": "America/New_York"
}))
);
assert_eq!(
encoded.pointer("/input/1/content/0/text"),
Some(&json!(normal_chat))
);
assert_eq!(
encoded.pointer("/input/2/content/0/text"),
Some(&json!(normal_chat))
);
assert_eq!(
encoded.pointer("/input/0/internal_chat_message_metadata_passthrough/create_time"),
Some(&json!(1789293131.822))
);
assert_eq!(
encoded.pointer("/tools/1/description"),
Some(&json!("Keep Asia/Shanghai unchanged"))
);
assert_eq!(
encoded.pointer("/client_metadata"),
Some(&json!({
"x-codex-turn-metadata": "{\"turn_started_at_unix_ms\":1789293131822}",
"x-codex-ws-stream-request-start-ms": "1789293132000"
}))
);
}
#[test]
fn encoder_should_preserve_client_store_intent() {
let request = request(Map::from_iter([