feat: 扩展 cache creation token 细分统计与 effective_input_tokens 计费逻辑

- 新增 cache_creation_ephemeral_5m/1h_input_tokens 字段,区分不同 TTL 的缓存写入 token
- 引入 effective_input_tokens(扣除 cache read 后的有效输入 token),暴露给 usage 接口
- billing 规则生成器支持 5m/1h ephemeral cache 独立定价与分级计费
- usage_mapper 增加 Claude/Anthropic 格式映射,修复 OpenAI responses 格式字段兼容性
- 迁移逻辑增强:支持 checksum 容错、applied/pending 数量日志、逐步执行信息输出
- executor 抽离 LocalExecutionRequestOutcome 类型,统一 sync/stream 路径返回语义
- provider-transport auth 层新增 complete passthrough headers 构建逻辑
- 前端 usage 类型全面补充 effective_input_tokens、cache_creation_tokens、total_input_context 字段
This commit is contained in:
fawney19
2026-04-10 17:44:55 +08:00
parent 5014e2f5fd
commit 010ab127e2
64 changed files with 4217 additions and 477 deletions
@@ -7,7 +7,9 @@ use crate::ai_pipeline::transport::antigravity::{
classify_local_antigravity_request_support, AntigravityEnvelopeRequestType,
AntigravityRequestEnvelopeSupport, AntigravityRequestSideSupport,
};
use crate::ai_pipeline::transport::auth::build_openai_passthrough_headers;
use crate::ai_pipeline::transport::auth::{
build_complete_passthrough_headers, build_complete_passthrough_headers_with_auth,
};
use crate::ai_pipeline::transport::claude_code::build_claude_code_passthrough_headers;
use crate::ai_pipeline::transport::kiro::{
build_kiro_provider_headers, KiroProviderHeadersInput, KIRO_ENVELOPE_NAME,
@@ -198,13 +200,13 @@ pub(crate) async fn maybe_build_local_same_format_provider_decision_payload_for_
transport.key.fingerprint.as_ref(),
)
} else if is_vertex {
crate::ai_pipeline::transport::build_passthrough_headers(
build_complete_passthrough_headers(
&parts.headers,
&extra_headers,
Some("application/json"),
)
} else {
build_openai_passthrough_headers(
build_complete_passthrough_headers_with_auth(
&parts.headers,
auth_header.as_deref().unwrap_or_default(),
auth_value.as_deref().unwrap_or_default(),
@@ -6,7 +6,7 @@ use tracing::warn;
use crate::ai_pipeline::planner::common::force_upstream_streaming_for_provider;
use crate::ai_pipeline::planner::standard::apply_codex_openai_cli_special_headers;
use crate::ai_pipeline::transport::auth::{
build_openai_passthrough_headers, ensure_upstream_auth_header,
build_claude_passthrough_headers, build_openai_passthrough_headers, ensure_upstream_auth_header,
};
use crate::ai_pipeline::transport::{
apply_local_header_rules, resolve_transport_execution_timeouts,
@@ -245,13 +245,23 @@ pub(super) async fn maybe_build_local_standard_decision_payload_for_candidate(
}
};
let mut provider_request_headers = build_openai_passthrough_headers(
&parts.headers,
&auth_header,
&auth_value,
&BTreeMap::new(),
Some("application/json"),
);
let mut provider_request_headers = if provider_api_format.starts_with("claude:") {
build_claude_passthrough_headers(
&parts.headers,
&auth_header,
&auth_value,
&BTreeMap::new(),
Some("application/json"),
)
} else {
build_openai_passthrough_headers(
&parts.headers,
&auth_header,
&auth_value,
&BTreeMap::new(),
Some("application/json"),
)
};
if !apply_local_header_rules(
&mut provider_request_headers,
transport.endpoint.header_rules.as_ref(),
@@ -1,6 +1,15 @@
use serde_json::json;
use serde_json::{json, Value};
use super::build_cross_format_openai_cli_request_body;
use super::{build_cross_format_openai_cli_request_body, build_local_openai_cli_request_body};
fn object_keys(value: &Value) -> Vec<&str> {
value
.as_object()
.expect("json object")
.keys()
.map(String::as_str)
.collect()
}
#[test]
fn builds_openai_chat_cross_format_request_body_from_openai_cli_source() {
@@ -26,6 +35,52 @@ fn builds_openai_chat_cross_format_request_body_from_openai_cli_source() {
assert_eq!(provider_request_body["messages"][0]["content"], "hello");
}
#[test]
fn local_openai_cli_wrapper_preserves_body_order_after_edits() {
let body_json: Value = serde_json::from_str(
r#"{
"text": {"format": {"type": "text"}},
"input": [],
"model": "gpt-5.4",
"store": false,
"tools": [],
"stream": true,
"include": ["reasoning.encrypted_content"],
"reasoning": {"effort": "high"},
"tool_choice": "auto"
}"#,
)
.expect("request body should parse");
let provider_request_body = build_local_openai_cli_request_body(
&body_json,
"gpt-5.4",
true,
"codex",
"openai:cli",
None,
Some("key-123"),
)
.expect("local openai cli body should build");
assert_eq!(
object_keys(&provider_request_body),
vec![
"text",
"input",
"model",
"store",
"tools",
"stream",
"include",
"reasoning",
"tool_choice",
"instructions",
"prompt_cache_key",
]
);
}
#[test]
fn strips_metadata_for_codex_openai_cli_requests() {
let body_json = json!({
@@ -16,7 +16,7 @@ use crate::ai_pipeline::planner::standard::{
build_cross_format_openai_chat_upstream_url,
};
use crate::ai_pipeline::transport::auth::{
build_openai_passthrough_headers, ensure_upstream_auth_header,
build_claude_passthrough_headers, build_openai_passthrough_headers, ensure_upstream_auth_header,
};
use crate::ai_pipeline::transport::{
apply_local_header_rules, resolve_transport_execution_timeouts,
@@ -182,13 +182,23 @@ pub(super) async fn build_cross_format_local_openai_chat_decision_payload_for_ca
return None;
};
let mut provider_request_headers = build_openai_passthrough_headers(
&parts.headers,
&auth_header,
&auth_value,
&BTreeMap::new(),
Some("application/json"),
);
let mut provider_request_headers = if provider_api_format.starts_with("claude:") {
build_claude_passthrough_headers(
&parts.headers,
&auth_header,
&auth_value,
&BTreeMap::new(),
Some("application/json"),
)
} else {
build_openai_passthrough_headers(
&parts.headers,
&auth_header,
&auth_value,
&BTreeMap::new(),
Some("application/json"),
)
};
if !apply_local_header_rules(
&mut provider_request_headers,
transport.endpoint.header_rules.as_ref(),
@@ -9,7 +9,8 @@ use crate::ai_pipeline::planner::standard::{
build_local_openai_chat_upstream_url,
};
use crate::ai_pipeline::transport::auth::{
build_openai_passthrough_headers, ensure_upstream_auth_header, resolve_local_openai_chat_auth,
build_complete_passthrough_headers_with_auth, ensure_upstream_auth_header,
resolve_local_openai_chat_auth,
};
use crate::ai_pipeline::transport::policy::supports_local_openai_chat_transport;
use crate::ai_pipeline::transport::{
@@ -140,7 +141,7 @@ pub(super) async fn build_same_format_local_openai_chat_decision_payload_for_can
return None;
};
let mut provider_request_headers = build_openai_passthrough_headers(
let mut provider_request_headers = build_complete_passthrough_headers_with_auth(
&parts.headers,
&auth_header,
&auth_value,
@@ -22,6 +22,7 @@ use crate::ai_pipeline::transport::antigravity::{
};
use crate::ai_pipeline::transport::apply_local_header_rules;
use crate::ai_pipeline::transport::auth::{
build_claude_passthrough_headers, build_complete_passthrough_headers_with_auth,
build_openai_passthrough_headers, ensure_upstream_auth_header, resolve_local_gemini_auth,
resolve_local_standard_auth,
};
@@ -354,16 +355,35 @@ pub(crate) async fn resolve_local_openai_cli_candidate_payload_parts(
return None;
};
let mut provider_request_headers = build_openai_passthrough_headers(
&parts.headers,
&auth_header,
&auth_value,
&antigravity_auth
.as_ref()
.map(build_antigravity_static_identity_headers)
.unwrap_or_default(),
Some("application/json"),
);
let extra_headers = antigravity_auth
.as_ref()
.map(build_antigravity_static_identity_headers)
.unwrap_or_default();
let mut provider_request_headers = if same_format {
build_complete_passthrough_headers_with_auth(
&parts.headers,
&auth_header,
&auth_value,
&extra_headers,
Some("application/json"),
)
} else if provider_api_format.starts_with("claude:") {
build_claude_passthrough_headers(
&parts.headers,
&auth_header,
&auth_value,
&extra_headers,
Some("application/json"),
)
} else {
build_openai_passthrough_headers(
&parts.headers,
&auth_header,
&auth_value,
&extra_headers,
Some("application/json"),
)
};
if !apply_local_header_rules(
&mut provider_request_headers,
transport.endpoint.header_rules.as_ref(),
@@ -6,6 +6,7 @@ use super::super::{
};
use crate::ai_pipeline::provider_adaptation_requires_eventstream_accept;
use crate::ai_pipeline::transport::auth::{
build_claude_passthrough_headers, build_complete_passthrough_headers_with_auth,
build_openai_passthrough_headers, ensure_upstream_auth_header,
};
use crate::ai_pipeline::transport::url::{build_openai_chat_url, build_openai_cli_url};
@@ -132,13 +133,31 @@ pub(crate) fn build_openai_chat_stream_plan_from_decision(
};
let mut provider_request_headers = if payload.provider_request_headers.is_empty() {
build_openai_passthrough_headers(
&parts.headers,
&auth_header,
&auth_value,
&payload.extra_headers,
payload.content_type.as_deref(),
)
if provider_api_format == client_api_format {
build_complete_passthrough_headers_with_auth(
&parts.headers,
&auth_header,
&auth_value,
&payload.extra_headers,
payload.content_type.as_deref(),
)
} else if provider_api_format.starts_with("claude:") {
build_claude_passthrough_headers(
&parts.headers,
&auth_header,
&auth_value,
&payload.extra_headers,
payload.content_type.as_deref(),
)
} else {
build_openai_passthrough_headers(
&parts.headers,
&auth_header,
&auth_value,
&payload.extra_headers,
payload.content_type.as_deref(),
)
}
} else {
payload.provider_request_headers.clone()
};
@@ -281,6 +300,11 @@ pub(crate) fn build_openai_cli_stream_plan_from_decision(
} else {
provider_request_headers.insert("accept".to_string(), "text/event-stream".to_string());
}
let report_context = augment_sync_report_context(
payload.report_context,
&provider_request_headers,
&provider_request_body_value,
)?;
let plan = ExecutionPlan {
request_id,
candidate_id: payload.candidate_id.clone(),
@@ -306,15 +330,276 @@ pub(crate) fn build_openai_cli_stream_plan_from_decision(
timeouts: payload.timeouts.clone(),
};
let report_context = augment_sync_report_context(
payload.report_context,
&plan.headers,
&provider_request_body_value,
)?;
Ok(Some(LocalStreamPlanAndReport {
plan,
report_kind: payload.report_kind,
report_context,
}))
}
#[cfg(test)]
mod tests {
use std::collections::BTreeMap;
use serde_json::{json, Value};
use super::{
build_openai_chat_stream_plan_from_decision, build_openai_cli_stream_plan_from_decision,
};
use crate::GatewayControlSyncDecisionResponse;
fn object_keys(value: &Value) -> Vec<&str> {
value
.as_object()
.expect("value should be an object")
.keys()
.map(String::as_str)
.collect()
}
fn sample_cli_payload() -> GatewayControlSyncDecisionResponse {
GatewayControlSyncDecisionResponse {
action: "stream".to_string(),
decision_kind: Some("openai_cli_stream".to_string()),
execution_strategy: None,
conversion_mode: None,
request_id: Some("req_123".to_string()),
candidate_id: Some("cand_123".to_string()),
provider_name: Some("Codex".to_string()),
provider_id: Some("prov_123".to_string()),
endpoint_id: Some("ep_123".to_string()),
key_id: Some("key_123".to_string()),
upstream_base_url: Some("https://example.com".to_string()),
upstream_url: Some("https://example.com/v1/responses".to_string()),
provider_request_method: None,
auth_header: Some("authorization".to_string()),
auth_value: Some("Bearer test".to_string()),
provider_api_format: Some("openai:cli".to_string()),
client_api_format: Some("openai:cli".to_string()),
provider_contract: Some("openai:cli".to_string()),
client_contract: Some("openai:cli".to_string()),
model_name: Some("gpt-5.4".to_string()),
mapped_model: Some("gpt-5.4".to_string()),
prompt_cache_key: Some("cache-key".to_string()),
extra_headers: BTreeMap::new(),
provider_request_headers: BTreeMap::from([(
"content-type".to_string(),
"application/json".to_string(),
)]),
provider_request_body: Some(json!({
"text": {"verbosity": "low"},
"input": [],
"model": "gpt-5.4",
"store": false,
"tools": [],
"stream": true,
"include": ["reasoning.encrypted_content"],
"reasoning": {"effort": "high"},
"tool_choice": "auto",
"instructions": "You are Codex.",
"prompt_cache_key": "cache-key"
})),
provider_request_body_base64: None,
content_type: Some("application/json".to_string()),
proxy: None,
tls_profile: None,
timeouts: None,
upstream_is_stream: true,
report_kind: Some("openai_cli_stream_success".to_string()),
report_context: Some(json!({})),
auth_context: None,
}
}
#[test]
fn build_openai_cli_stream_plan_preserves_provider_request_body_order_in_plan_and_report() {
let parts = http::Request::builder()
.uri("http://localhost/v1/responses")
.body(())
.expect("request should build")
.into_parts()
.0;
let payload = sample_cli_payload();
let built = build_openai_cli_stream_plan_from_decision(&parts, &json!({}), payload, false)
.expect("plan build should succeed")
.expect("plan should be produced");
let plan_body = built
.plan
.body
.json_body
.as_ref()
.expect("plan json body should exist");
assert_eq!(
object_keys(plan_body),
vec![
"text",
"input",
"model",
"store",
"tools",
"stream",
"include",
"reasoning",
"tool_choice",
"instructions",
"prompt_cache_key",
]
);
let report_context = built
.report_context
.as_ref()
.and_then(|value| value.get("provider_request_body"))
.expect("report context should contain provider request body");
assert_eq!(object_keys(report_context), object_keys(plan_body));
}
#[test]
fn build_openai_chat_stream_plan_fallback_preserves_complete_same_format_headers() {
let parts = http::Request::builder()
.uri("http://localhost/v1/chat/completions")
.header(http::header::AUTHORIZATION, "Bearer client-token")
.header("x-stainless-runtime-version", "v24.0.0")
.header("x-app", "codex")
.body(())
.expect("request should build")
.into_parts()
.0;
let payload = GatewayControlSyncDecisionResponse {
action: "stream".to_string(),
decision_kind: Some("openai_chat_stream".to_string()),
execution_strategy: None,
conversion_mode: None,
request_id: Some("req_stream_456".to_string()),
candidate_id: Some("cand_stream_456".to_string()),
provider_name: Some("OpenAI".to_string()),
provider_id: Some("prov_stream_456".to_string()),
endpoint_id: Some("ep_stream_456".to_string()),
key_id: Some("key_stream_456".to_string()),
upstream_base_url: Some("https://example.com".to_string()),
upstream_url: Some("https://example.com/v1/chat/completions".to_string()),
provider_request_method: None,
auth_header: Some("authorization".to_string()),
auth_value: Some("Bearer upstream-token".to_string()),
provider_api_format: Some("openai:chat".to_string()),
client_api_format: Some("openai:chat".to_string()),
provider_contract: Some("openai:chat".to_string()),
client_contract: Some("openai:chat".to_string()),
model_name: Some("gpt-5.4".to_string()),
mapped_model: Some("gpt-5.4".to_string()),
prompt_cache_key: None,
extra_headers: BTreeMap::new(),
provider_request_headers: BTreeMap::new(),
provider_request_body: Some(json!({"model":"gpt-5.4","messages":[],"stream":true})),
provider_request_body_base64: None,
content_type: Some("application/json".to_string()),
proxy: None,
tls_profile: None,
timeouts: None,
upstream_is_stream: true,
report_kind: Some("openai_chat_stream_success".to_string()),
report_context: Some(json!({})),
auth_context: None,
};
let built = build_openai_chat_stream_plan_from_decision(&parts, &json!({}), payload)
.expect("plan build should succeed")
.expect("plan should be produced");
assert_eq!(
built.plan.headers.get("authorization").map(String::as_str),
Some("Bearer upstream-token")
);
assert_eq!(
built
.plan
.headers
.get("x-stainless-runtime-version")
.map(String::as_str),
Some("v24.0.0")
);
assert_eq!(
built.plan.headers.get("x-app").map(String::as_str),
Some("codex")
);
assert_eq!(
built.plan.headers.get("accept").map(String::as_str),
Some("text/event-stream")
);
}
#[test]
fn build_openai_chat_stream_plan_fallback_restores_claude_headers_for_cross_format() {
let parts = http::Request::builder()
.uri("http://localhost/v1/chat/completions")
.header("anthropic-beta", "prompt-caching-2024-07-31")
.header("x-stainless-runtime-version", "v24.0.0")
.body(())
.expect("request should build")
.into_parts()
.0;
let payload = GatewayControlSyncDecisionResponse {
action: "stream".to_string(),
decision_kind: Some("openai_chat_stream".to_string()),
execution_strategy: None,
conversion_mode: Some("format_conversion".to_string()),
request_id: Some("req_stream_789".to_string()),
candidate_id: Some("cand_stream_789".to_string()),
provider_name: Some("Claude".to_string()),
provider_id: Some("prov_stream_789".to_string()),
endpoint_id: Some("ep_stream_789".to_string()),
key_id: Some("key_stream_789".to_string()),
upstream_base_url: Some("https://example.com".to_string()),
upstream_url: Some("https://example.com/v1/messages".to_string()),
provider_request_method: None,
auth_header: Some("x-api-key".to_string()),
auth_value: Some("sk-upstream-claude".to_string()),
provider_api_format: Some("claude:chat".to_string()),
client_api_format: Some("openai:chat".to_string()),
provider_contract: Some("claude:chat".to_string()),
client_contract: Some("openai:chat".to_string()),
model_name: Some("claude-sonnet-4-5".to_string()),
mapped_model: Some("claude-sonnet-4-5".to_string()),
prompt_cache_key: None,
extra_headers: BTreeMap::new(),
provider_request_headers: BTreeMap::new(),
provider_request_body: Some(
json!({"model":"claude-sonnet-4-5","messages":[],"stream":true}),
),
provider_request_body_base64: None,
content_type: Some("application/json".to_string()),
proxy: None,
tls_profile: None,
timeouts: None,
upstream_is_stream: true,
report_kind: Some("openai_chat_stream_success".to_string()),
report_context: Some(json!({})),
auth_context: None,
};
let built = build_openai_chat_stream_plan_from_decision(&parts, &json!({}), payload)
.expect("plan build should succeed")
.expect("plan should be produced");
assert_eq!(
built.plan.headers.get("x-api-key").map(String::as_str),
Some("sk-upstream-claude")
);
assert_eq!(
built.plan.headers.get("anthropic-beta").map(String::as_str),
Some("prompt-caching-2024-07-31")
);
assert_eq!(
built
.plan
.headers
.get("anthropic-version")
.map(String::as_str),
Some("2023-06-01")
);
assert_eq!(
built.plan.headers.get("accept").map(String::as_str),
Some("text/event-stream")
);
}
}
@@ -5,6 +5,7 @@ use super::super::{
LocalSyncPlanAndReport,
};
use crate::ai_pipeline::transport::auth::{
build_claude_passthrough_headers, build_complete_passthrough_headers_with_auth,
build_openai_passthrough_headers, ensure_upstream_auth_header,
};
use crate::ai_pipeline::transport::url::{build_openai_chat_url, build_openai_cli_url};
@@ -132,13 +133,31 @@ pub(crate) fn build_openai_chat_sync_plan_from_decision(
};
let mut provider_request_headers = if payload.provider_request_headers.is_empty() {
build_openai_passthrough_headers(
&parts.headers,
&auth_header,
&auth_value,
&payload.extra_headers,
payload.content_type.as_deref(),
)
if provider_api_format == client_api_format {
build_complete_passthrough_headers_with_auth(
&parts.headers,
&auth_header,
&auth_value,
&payload.extra_headers,
payload.content_type.as_deref(),
)
} else if provider_api_format.starts_with("claude:") {
build_claude_passthrough_headers(
&parts.headers,
&auth_header,
&auth_value,
&payload.extra_headers,
payload.content_type.as_deref(),
)
} else {
build_openai_passthrough_headers(
&parts.headers,
&auth_header,
&auth_value,
&payload.extra_headers,
payload.content_type.as_deref(),
)
}
} else {
payload.provider_request_headers.clone()
};
@@ -275,6 +294,11 @@ pub(crate) fn build_openai_cli_sync_plan_from_decision(
if payload.upstream_is_stream && !provider_request_headers.contains_key("accept") {
provider_request_headers.insert("accept".to_string(), "text/event-stream".to_string());
}
let report_context = augment_sync_report_context(
payload.report_context,
&provider_request_headers,
&provider_request_body_value,
)?;
let plan = ExecutionPlan {
request_id,
candidate_id: payload.candidate_id.clone(),
@@ -300,15 +324,268 @@ pub(crate) fn build_openai_cli_sync_plan_from_decision(
timeouts: payload.timeouts.clone(),
};
let report_context = augment_sync_report_context(
payload.report_context,
&plan.headers,
&provider_request_body_value,
)?;
Ok(Some(LocalSyncPlanAndReport {
plan,
report_kind: payload.report_kind,
report_context,
}))
}
#[cfg(test)]
mod tests {
use std::collections::BTreeMap;
use serde_json::{json, Value};
use super::{
build_openai_chat_sync_plan_from_decision, build_openai_cli_sync_plan_from_decision,
};
use crate::GatewayControlSyncDecisionResponse;
fn object_keys(value: &Value) -> Vec<&str> {
value
.as_object()
.expect("value should be an object")
.keys()
.map(String::as_str)
.collect()
}
fn sample_cli_payload() -> GatewayControlSyncDecisionResponse {
GatewayControlSyncDecisionResponse {
action: "sync".to_string(),
decision_kind: Some("openai_cli_sync".to_string()),
execution_strategy: None,
conversion_mode: None,
request_id: Some("req_123".to_string()),
candidate_id: Some("cand_123".to_string()),
provider_name: Some("Codex".to_string()),
provider_id: Some("prov_123".to_string()),
endpoint_id: Some("ep_123".to_string()),
key_id: Some("key_123".to_string()),
upstream_base_url: Some("https://example.com".to_string()),
upstream_url: Some("https://example.com/v1/responses".to_string()),
provider_request_method: None,
auth_header: Some("authorization".to_string()),
auth_value: Some("Bearer test".to_string()),
provider_api_format: Some("openai:cli".to_string()),
client_api_format: Some("openai:cli".to_string()),
provider_contract: Some("openai:cli".to_string()),
client_contract: Some("openai:cli".to_string()),
model_name: Some("gpt-5.4".to_string()),
mapped_model: Some("gpt-5.4".to_string()),
prompt_cache_key: Some("cache-key".to_string()),
extra_headers: BTreeMap::new(),
provider_request_headers: BTreeMap::from([(
"content-type".to_string(),
"application/json".to_string(),
)]),
provider_request_body: Some(json!({
"text": {"verbosity": "low"},
"input": [],
"model": "gpt-5.4",
"store": false,
"tools": [],
"stream": true,
"include": ["reasoning.encrypted_content"],
"reasoning": {"effort": "high"},
"tool_choice": "auto",
"instructions": "You are Codex.",
"prompt_cache_key": "cache-key"
})),
provider_request_body_base64: None,
content_type: Some("application/json".to_string()),
proxy: None,
tls_profile: None,
timeouts: None,
upstream_is_stream: true,
report_kind: Some("openai_cli_sync_success".to_string()),
report_context: Some(json!({})),
auth_context: None,
}
}
#[test]
fn build_openai_cli_sync_plan_preserves_provider_request_body_order_in_plan_and_report() {
let parts = http::Request::builder()
.uri("http://localhost/v1/responses")
.body(())
.expect("request should build")
.into_parts()
.0;
let payload = sample_cli_payload();
let built = build_openai_cli_sync_plan_from_decision(&parts, &json!({}), payload, false)
.expect("plan build should succeed")
.expect("plan should be produced");
let plan_body = built
.plan
.body
.json_body
.as_ref()
.expect("plan json body should exist");
assert_eq!(
object_keys(plan_body),
vec![
"text",
"input",
"model",
"store",
"tools",
"stream",
"include",
"reasoning",
"tool_choice",
"instructions",
"prompt_cache_key",
]
);
let report_context = built
.report_context
.as_ref()
.and_then(|value| value.get("provider_request_body"))
.expect("report context should contain provider request body");
assert_eq!(object_keys(report_context), object_keys(plan_body));
}
#[test]
fn build_openai_chat_sync_plan_fallback_preserves_complete_same_format_headers() {
let parts = http::Request::builder()
.uri("http://localhost/v1/chat/completions")
.header(http::header::AUTHORIZATION, "Bearer client-token")
.header("x-stainless-runtime-version", "v24.0.0")
.header("x-app", "codex")
.body(())
.expect("request should build")
.into_parts()
.0;
let payload = GatewayControlSyncDecisionResponse {
action: "sync".to_string(),
decision_kind: Some("openai_chat_sync".to_string()),
execution_strategy: None,
conversion_mode: None,
request_id: Some("req_456".to_string()),
candidate_id: Some("cand_456".to_string()),
provider_name: Some("OpenAI".to_string()),
provider_id: Some("prov_456".to_string()),
endpoint_id: Some("ep_456".to_string()),
key_id: Some("key_456".to_string()),
upstream_base_url: Some("https://example.com".to_string()),
upstream_url: Some("https://example.com/v1/chat/completions".to_string()),
provider_request_method: None,
auth_header: Some("authorization".to_string()),
auth_value: Some("Bearer upstream-token".to_string()),
provider_api_format: Some("openai:chat".to_string()),
client_api_format: Some("openai:chat".to_string()),
provider_contract: Some("openai:chat".to_string()),
client_contract: Some("openai:chat".to_string()),
model_name: Some("gpt-5.4".to_string()),
mapped_model: Some("gpt-5.4".to_string()),
prompt_cache_key: None,
extra_headers: BTreeMap::new(),
provider_request_headers: BTreeMap::new(),
provider_request_body: Some(json!({"model":"gpt-5.4","messages":[],"stream":false})),
provider_request_body_base64: None,
content_type: Some("application/json".to_string()),
proxy: None,
tls_profile: None,
timeouts: None,
upstream_is_stream: false,
report_kind: Some("openai_chat_sync_success".to_string()),
report_context: Some(json!({})),
auth_context: None,
};
let built = build_openai_chat_sync_plan_from_decision(&parts, &json!({}), payload)
.expect("plan build should succeed")
.expect("plan should be produced");
assert_eq!(
built.plan.headers.get("authorization").map(String::as_str),
Some("Bearer upstream-token")
);
assert_eq!(
built
.plan
.headers
.get("x-stainless-runtime-version")
.map(String::as_str),
Some("v24.0.0")
);
assert_eq!(
built.plan.headers.get("x-app").map(String::as_str),
Some("codex")
);
}
#[test]
fn build_openai_chat_sync_plan_fallback_restores_claude_headers_for_cross_format() {
let parts = http::Request::builder()
.uri("http://localhost/v1/chat/completions")
.header("anthropic-beta", "prompt-caching-2024-07-31")
.header("x-stainless-runtime-version", "v24.0.0")
.body(())
.expect("request should build")
.into_parts()
.0;
let payload = GatewayControlSyncDecisionResponse {
action: "sync".to_string(),
decision_kind: Some("openai_chat_sync".to_string()),
execution_strategy: None,
conversion_mode: Some("format_conversion".to_string()),
request_id: Some("req_789".to_string()),
candidate_id: Some("cand_789".to_string()),
provider_name: Some("Claude".to_string()),
provider_id: Some("prov_789".to_string()),
endpoint_id: Some("ep_789".to_string()),
key_id: Some("key_789".to_string()),
upstream_base_url: Some("https://example.com".to_string()),
upstream_url: Some("https://example.com/v1/messages".to_string()),
provider_request_method: None,
auth_header: Some("x-api-key".to_string()),
auth_value: Some("sk-upstream-claude".to_string()),
provider_api_format: Some("claude:chat".to_string()),
client_api_format: Some("openai:chat".to_string()),
provider_contract: Some("claude:chat".to_string()),
client_contract: Some("openai:chat".to_string()),
model_name: Some("claude-sonnet-4-5".to_string()),
mapped_model: Some("claude-sonnet-4-5".to_string()),
prompt_cache_key: None,
extra_headers: BTreeMap::new(),
provider_request_headers: BTreeMap::new(),
provider_request_body: Some(
json!({"model":"claude-sonnet-4-5","messages":[],"stream":false}),
),
provider_request_body_base64: None,
content_type: Some("application/json".to_string()),
proxy: None,
tls_profile: None,
timeouts: None,
upstream_is_stream: false,
report_kind: Some("openai_chat_sync_success".to_string()),
report_context: Some(json!({})),
auth_context: None,
};
let built = build_openai_chat_sync_plan_from_decision(&parts, &json!({}), payload)
.expect("plan build should succeed")
.expect("plan should be produced");
assert_eq!(
built.plan.headers.get("x-api-key").map(String::as_str),
Some("sk-upstream-claude")
);
assert_eq!(
built.plan.headers.get("anthropic-beta").map(String::as_str),
Some("prompt-caching-2024-07-31")
);
assert_eq!(
built
.plan
.headers
.get("anthropic-version")
.map(String::as_str),
Some("2023-06-01")
);
}
}