mirror of
https://github.com/fawney19/Aether.git
synced 2026-10-09 02:47:45 +08:00
feat(openai): align GPT-5.6 and Codex request contracts
This commit is contained in:
@@ -14,7 +14,10 @@ use serde_json::{json, Value};
|
||||
use tracing::warn;
|
||||
|
||||
use crate::ai_serving::planner::common::extract_standard_requested_model;
|
||||
use crate::ai_serving::{ExecutionRuntimeAuthContext, GatewayAuthApiKeySnapshot, PlannerAppState};
|
||||
use crate::ai_serving::{
|
||||
ExecutionRuntimeAuthContext, GatewayAuthApiKeySnapshot, GatewayProviderTransportSnapshot,
|
||||
PlannerAppState,
|
||||
};
|
||||
use crate::client_session_affinity::client_session_affinity_from_request;
|
||||
use crate::clock::current_unix_secs;
|
||||
use crate::routing::{
|
||||
@@ -27,12 +30,16 @@ use crate::stage_metrics::observe_gateway_stage_ms;
|
||||
use crate::{AiExecutionDecision, AppState, GatewayError};
|
||||
|
||||
const ROUTING_GROUP_SELECTION_CACHE_TTL: Duration = Duration::from_secs(30);
|
||||
const CODEX_ACCOUNT_ID_HEADER: &str = "chatgpt-account-id";
|
||||
const CODEX_FEDRAMP_HEADER: &str = "x-openai-fedramp";
|
||||
const CODEX_RESPONSES_LITE_HEADER: &str = "x-openai-internal-codex-responses-lite";
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
pub(crate) struct ResolvedLocalDecisionAuthInput {
|
||||
pub(crate) auth_context: ExecutionRuntimeAuthContext,
|
||||
pub(crate) auth_snapshot: GatewayAuthApiKeySnapshot,
|
||||
pub(crate) required_capabilities: Option<serde_json::Value>,
|
||||
pub(crate) model_directive_policy: crate::system_features::ModelDirectivePolicySnapshot,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
@@ -46,6 +53,7 @@ pub(crate) struct LocalRequestedModelDecisionInput {
|
||||
pub(crate) routing_policy: Option<ResolvedRoutingPolicy>,
|
||||
pub(crate) routing_trace_seed: Option<RoutingDecisionTrace>,
|
||||
pub(crate) routing_context: Option<LocalRoutingRequestContext>,
|
||||
pub(crate) model_directive_policy: crate::system_features::ModelDirectivePolicySnapshot,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
@@ -86,14 +94,52 @@ impl LocalRequestedModelDecisionInput {
|
||||
pub(crate) fn apply_provider_request_routing_policy_to_decision(
|
||||
input: &LocalRequestedModelDecisionInput,
|
||||
decision: &mut AiExecutionDecision,
|
||||
transport: Option<&GatewayProviderTransportSnapshot>,
|
||||
) -> Result<(), GatewayError> {
|
||||
let provider_api_format = decision
|
||||
.provider_api_format
|
||||
.clone()
|
||||
.or_else(|| {
|
||||
input
|
||||
.routing_context
|
||||
.as_ref()
|
||||
.map(|context| context.client_api_format.clone())
|
||||
})
|
||||
.unwrap_or_default();
|
||||
let provider_type = decision.provider_type.clone().unwrap_or_default();
|
||||
let terminal_provider_model = decision
|
||||
.provider_request_body
|
||||
.as_ref()
|
||||
.and_then(|body| body.get("model"))
|
||||
.and_then(Value::as_str)
|
||||
.or(decision.mapped_model.as_deref())
|
||||
.or(decision.model_name.as_deref())
|
||||
.unwrap_or(input.requested_model.as_str());
|
||||
let model_capabilities = transport.and_then(|transport| {
|
||||
crate::ai_serving::codex_model_capabilities_for_transport(
|
||||
transport,
|
||||
provider_api_format.as_str(),
|
||||
terminal_provider_model,
|
||||
input.requested_model.as_str(),
|
||||
)
|
||||
});
|
||||
crate::ai_serving::apply_codex_openai_responses_lite_header_with_capabilities(
|
||||
&mut decision.provider_request_headers,
|
||||
provider_type.as_str(),
|
||||
provider_api_format.as_str(),
|
||||
terminal_provider_model,
|
||||
input.requested_model.as_str(),
|
||||
model_capabilities.as_ref(),
|
||||
);
|
||||
|
||||
let Some(context) = input.routing_context.as_ref() else {
|
||||
return Ok(());
|
||||
};
|
||||
let provider_api_format = decision
|
||||
.provider_api_format
|
||||
.as_deref()
|
||||
.unwrap_or(context.client_api_format.as_str());
|
||||
let provider_body_rules = decision
|
||||
.report_context
|
||||
.as_ref()
|
||||
.and_then(|context| context.get("body_rules"))
|
||||
.cloned();
|
||||
let resolved_model = decision
|
||||
.mapped_model
|
||||
.as_deref()
|
||||
@@ -104,6 +150,21 @@ pub(crate) fn apply_provider_request_routing_policy_to_decision(
|
||||
.clone()
|
||||
.unwrap_or(serde_json::Value::Null);
|
||||
let mut provider_headers = btree_headers_to_header_map(&decision.provider_request_headers)?;
|
||||
let mut protected_codex_header_names = vec![CODEX_ACCOUNT_ID_HEADER, CODEX_FEDRAMP_HEADER];
|
||||
if provider_type.eq_ignore_ascii_case("codex")
|
||||
&& crate::ai_serving::is_openai_responses_family_format(provider_api_format.as_str())
|
||||
{
|
||||
protected_codex_header_names.extend([
|
||||
"x-client-request-id",
|
||||
"accept",
|
||||
"content-encoding",
|
||||
CODEX_RESPONSES_LITE_HEADER,
|
||||
]);
|
||||
}
|
||||
let protected_codex_headers = protected_codex_header_names
|
||||
.into_iter()
|
||||
.map(|name| (name, provider_headers.get(name).cloned()))
|
||||
.collect::<Vec<_>>();
|
||||
let provider_headers_json = headers_to_routing_value(&provider_headers);
|
||||
let policy = resolve_gateway_routing_policy(GatewayRoutingPolicyInput {
|
||||
group_id: context.group_id.as_deref(),
|
||||
@@ -112,7 +173,7 @@ pub(crate) fn apply_provider_request_routing_policy_to_decision(
|
||||
selection_source: context.selection_source.as_str(),
|
||||
requested_model: input.requested_model.as_str(),
|
||||
resolved_model,
|
||||
api_format: provider_api_format,
|
||||
api_format: provider_api_format.as_str(),
|
||||
user_id: Some(input.auth_context.user_id.as_str()),
|
||||
api_key_id: Some(input.auth_context.api_key_id.as_str()),
|
||||
headers: &provider_headers_json,
|
||||
@@ -134,6 +195,80 @@ pub(crate) fn apply_provider_request_routing_policy_to_decision(
|
||||
&mut provider_headers,
|
||||
&policy.mutation_plan,
|
||||
)?;
|
||||
for (name, value) in protected_codex_headers {
|
||||
provider_headers.remove(name);
|
||||
if let Some(value) = value {
|
||||
provider_headers.insert(HeaderName::from_static(name), value);
|
||||
}
|
||||
}
|
||||
if original_provider_request_body.is_some() {
|
||||
let provider_model = provider_request_body
|
||||
.get("model")
|
||||
.and_then(Value::as_str)
|
||||
.map(str::trim)
|
||||
.filter(|value| !value.is_empty())
|
||||
.or(decision.mapped_model.as_deref())
|
||||
.or(decision.model_name.as_deref())
|
||||
.unwrap_or(input.requested_model.as_str())
|
||||
.to_string();
|
||||
let model_capabilities = transport.and_then(|transport| {
|
||||
crate::ai_serving::codex_model_capabilities_for_transport(
|
||||
transport,
|
||||
provider_api_format.as_str(),
|
||||
provider_model.as_str(),
|
||||
input.requested_model.as_str(),
|
||||
)
|
||||
});
|
||||
crate::ai_serving::finalize_openai_provider_request_with_codex_model_capabilities(
|
||||
&mut provider_request_body,
|
||||
crate::ai_serving::OpenAiProviderRequestFinalization {
|
||||
source_api_format: context.client_api_format.as_str(),
|
||||
provider_api_format: provider_api_format.as_str(),
|
||||
provider_type: provider_type.as_str(),
|
||||
provider_model: provider_model.as_str(),
|
||||
source_model: input.requested_model.as_str(),
|
||||
body_rules: provider_body_rules.as_ref(),
|
||||
upstream_is_stream: decision.upstream_is_stream,
|
||||
require_body_stream_field: original_provider_request_body
|
||||
.as_ref()
|
||||
.is_some_and(|body| body.get("stream").is_some()),
|
||||
},
|
||||
model_capabilities.as_ref(),
|
||||
)
|
||||
.map_err(|violation| GatewayError::Client {
|
||||
status: StatusCode::BAD_REQUEST,
|
||||
message: format!("routing provider_request violates provider contract: {violation:?}"),
|
||||
})?;
|
||||
}
|
||||
let provider_model = provider_request_body
|
||||
.get("model")
|
||||
.and_then(Value::as_str)
|
||||
.or(decision.mapped_model.as_deref())
|
||||
.or(decision.model_name.as_deref())
|
||||
.unwrap_or(input.requested_model.as_str());
|
||||
let mut provider_request_headers = header_map_to_btree_headers(&provider_headers);
|
||||
let model_capabilities = transport.and_then(|transport| {
|
||||
crate::ai_serving::codex_model_capabilities_for_transport(
|
||||
transport,
|
||||
provider_api_format.as_str(),
|
||||
provider_model,
|
||||
input.requested_model.as_str(),
|
||||
)
|
||||
});
|
||||
crate::ai_serving::apply_codex_openai_responses_lite_header_with_capabilities(
|
||||
&mut provider_request_headers,
|
||||
provider_type.as_str(),
|
||||
provider_api_format.as_str(),
|
||||
provider_model,
|
||||
input.requested_model.as_str(),
|
||||
model_capabilities.as_ref(),
|
||||
);
|
||||
crate::ai_serving::apply_codex_openai_compact_terminal_headers(
|
||||
&mut provider_request_headers,
|
||||
provider_type.as_str(),
|
||||
provider_api_format.as_str(),
|
||||
);
|
||||
provider_headers = btree_headers_to_header_map(&provider_request_headers)?;
|
||||
decision.provider_request_headers = header_map_to_btree_headers(&provider_headers);
|
||||
if original_provider_request_body.is_some() {
|
||||
decision.provider_request_body = Some(provider_request_body);
|
||||
@@ -145,6 +280,8 @@ pub(crate) fn apply_provider_request_routing_policy_to_decision(
|
||||
struct GatewayAuthenticatedDecisionInputPort<'a> {
|
||||
state: PlannerAppState<'a>,
|
||||
now_unix_secs: u64,
|
||||
model_directive_policy: &'a crate::system_features::ModelDirectivePolicySnapshot,
|
||||
model_directive_base_model: Option<String>,
|
||||
}
|
||||
|
||||
#[async_trait]
|
||||
@@ -181,6 +318,7 @@ impl AiAuthenticatedDecisionInputPort for GatewayAuthenticatedDecisionInputPort<
|
||||
&auth_context.api_key_id,
|
||||
requested_model,
|
||||
explicit_required_capabilities,
|
||||
self.model_directive_base_model.as_deref(),
|
||||
)
|
||||
.await)
|
||||
}
|
||||
@@ -195,6 +333,7 @@ impl AiAuthenticatedDecisionInputPort for GatewayAuthenticatedDecisionInputPort<
|
||||
auth_context,
|
||||
auth_snapshot,
|
||||
required_capabilities,
|
||||
model_directive_policy: self.model_directive_policy.clone(),
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -213,6 +352,7 @@ pub(crate) fn build_local_requested_model_decision_input(
|
||||
routing_policy: None,
|
||||
routing_trace_seed: None,
|
||||
routing_context: None,
|
||||
model_directive_policy: resolved_input.model_directive_policy,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -373,12 +513,16 @@ pub(crate) async fn attach_routing_policy_to_local_requested_model_input(
|
||||
}
|
||||
}
|
||||
if requested_model_changed {
|
||||
let model_directive_resolution = input
|
||||
.model_directive_policy
|
||||
.resolve_reasoning(client_api_format, Some(input.requested_model.as_str()));
|
||||
input.required_capabilities = PlannerAppState::new(state)
|
||||
.resolve_request_candidate_required_capabilities(
|
||||
&input.auth_context.user_id,
|
||||
&input.auth_context.api_key_id,
|
||||
Some(input.requested_model.as_str()),
|
||||
input.required_capabilities.as_ref(),
|
||||
model_directive_resolution.base_model(),
|
||||
)
|
||||
.await;
|
||||
}
|
||||
@@ -475,11 +619,22 @@ pub(crate) async fn resolve_local_authenticated_decision_input(
|
||||
state: &AppState,
|
||||
auth_context: ExecutionRuntimeAuthContext,
|
||||
requested_model: Option<&str>,
|
||||
requested_model_api_format: Option<&str>,
|
||||
explicit_required_capabilities: Option<&serde_json::Value>,
|
||||
model_directive_policy: &crate::system_features::ModelDirectivePolicySnapshot,
|
||||
) -> Result<Option<ResolvedLocalDecisionAuthInput>, GatewayError> {
|
||||
let model_directive_base_model = match (requested_model, requested_model_api_format) {
|
||||
(Some(model), Some(api_format)) => model_directive_policy
|
||||
.resolve_reasoning(api_format, Some(model))
|
||||
.base_model()
|
||||
.map(str::to_owned),
|
||||
_ => None,
|
||||
};
|
||||
let port = GatewayAuthenticatedDecisionInputPort {
|
||||
state: PlannerAppState::new(state),
|
||||
now_unix_secs: current_unix_secs(),
|
||||
model_directive_policy,
|
||||
model_directive_base_model,
|
||||
};
|
||||
|
||||
run_ai_authenticated_decision_input(
|
||||
@@ -730,6 +885,10 @@ fn ensure_report_context_routing_trace(
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use aether_provider_transport::snapshot::{
|
||||
GatewayProviderTransportEndpoint, GatewayProviderTransportKey,
|
||||
GatewayProviderTransportProvider,
|
||||
};
|
||||
|
||||
fn sample_auth_context() -> ExecutionRuntimeAuthContext {
|
||||
ExecutionRuntimeAuthContext {
|
||||
@@ -782,6 +941,7 @@ mod tests {
|
||||
client_session_affinity: None,
|
||||
routing_policy: None,
|
||||
routing_trace_seed: None,
|
||||
model_directive_policy: Default::default(),
|
||||
routing_context: Some(LocalRoutingRequestContext {
|
||||
group_id: Some("group-1".to_string()),
|
||||
group_version: Some(3),
|
||||
@@ -830,6 +990,7 @@ mod tests {
|
||||
request_id: Some("trace-1".to_string()),
|
||||
candidate_id: Some("candidate-1".to_string()),
|
||||
provider_name: Some("provider".to_string()),
|
||||
provider_type: Some("openai".to_string()),
|
||||
provider_id: Some("provider-1".to_string()),
|
||||
endpoint_id: Some("endpoint-1".to_string()),
|
||||
key_id: Some("key-1".to_string()),
|
||||
@@ -869,9 +1030,80 @@ mod tests {
|
||||
}
|
||||
}
|
||||
|
||||
fn set_provider_request_rules(input: &mut LocalRequestedModelDecisionInput, actions: Value) {
|
||||
fn sample_codex_transport_with_card() -> GatewayProviderTransportSnapshot {
|
||||
let card = json!({
|
||||
"id": "gpt-future-agent",
|
||||
"slug": "gpt-future-agent",
|
||||
"use_responses_lite": true,
|
||||
"supports_reasoning_summaries": true,
|
||||
"default_reasoning_level": "low",
|
||||
"default_reasoning_summary": "none",
|
||||
"supported_reasoning_levels": [{"effort": "low"}, {"effort": "high"}]
|
||||
});
|
||||
GatewayProviderTransportSnapshot {
|
||||
provider: GatewayProviderTransportProvider {
|
||||
id: "provider-codex".to_string(),
|
||||
name: "Codex".to_string(),
|
||||
provider_type: "codex".to_string(),
|
||||
website: None,
|
||||
is_active: true,
|
||||
keep_priority_on_conversion: false,
|
||||
enable_format_conversion: true,
|
||||
concurrent_limit: None,
|
||||
max_retries: None,
|
||||
proxy: None,
|
||||
request_timeout_secs: None,
|
||||
stream_first_byte_timeout_secs: None,
|
||||
config: None,
|
||||
},
|
||||
endpoint: GatewayProviderTransportEndpoint {
|
||||
id: "endpoint-codex".to_string(),
|
||||
provider_id: "provider-codex".to_string(),
|
||||
api_format: "openai:responses:compact".to_string(),
|
||||
api_family: Some("openai".to_string()),
|
||||
endpoint_kind: Some("compact".to_string()),
|
||||
is_active: true,
|
||||
base_url: "https://chatgpt.com/backend-api/codex".to_string(),
|
||||
header_rules: None,
|
||||
body_rules: None,
|
||||
max_retries: None,
|
||||
custom_path: None,
|
||||
config: None,
|
||||
format_acceptance_config: None,
|
||||
proxy: None,
|
||||
},
|
||||
key: GatewayProviderTransportKey {
|
||||
id: "key-codex".to_string(),
|
||||
provider_id: "provider-codex".to_string(),
|
||||
name: "Codex key".to_string(),
|
||||
auth_type: "oauth".to_string(),
|
||||
is_active: true,
|
||||
api_formats: Some(vec!["openai:responses:compact".to_string()]),
|
||||
auth_type_by_format: None,
|
||||
allow_auth_channel_mismatch_formats: None,
|
||||
allowed_models: Some(vec!["gpt-future-agent".to_string()]),
|
||||
capabilities: None,
|
||||
rate_multipliers: None,
|
||||
global_priority_by_format: None,
|
||||
expires_at_unix_secs: None,
|
||||
proxy: None,
|
||||
fingerprint: None,
|
||||
upstream_metadata: Some(crate::ai_serving::build_codex_model_catalog_metadata(&[
|
||||
card,
|
||||
])),
|
||||
decrypted_api_key: "access-token".to_string(),
|
||||
decrypted_auth_config: None,
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
fn set_provider_request_rules(
|
||||
input: &mut LocalRequestedModelDecisionInput,
|
||||
allowed_models: &[&str],
|
||||
actions: Value,
|
||||
) {
|
||||
let config = json!({
|
||||
"allowed_models": ["gpt-5"],
|
||||
"allowed_models": allowed_models,
|
||||
"rules": [{
|
||||
"id": "provider-patch",
|
||||
"priority": 1,
|
||||
@@ -904,6 +1136,7 @@ mod tests {
|
||||
client_session_affinity: None,
|
||||
routing_policy: None,
|
||||
routing_trace_seed: None,
|
||||
model_directive_policy: Default::default(),
|
||||
routing_context: Some(LocalRoutingRequestContext {
|
||||
group_id: Some("stale".to_string()),
|
||||
group_version: Some(1),
|
||||
@@ -971,6 +1204,7 @@ mod tests {
|
||||
routing_policy: None,
|
||||
routing_trace_seed: None,
|
||||
routing_context: None,
|
||||
model_directive_policy: Default::default(),
|
||||
};
|
||||
let group_config_json = json!({
|
||||
"rules": [{
|
||||
@@ -1006,7 +1240,7 @@ mod tests {
|
||||
let input = sample_decision_input();
|
||||
let mut decision = sample_decision();
|
||||
|
||||
apply_provider_request_routing_policy_to_decision(&input, &mut decision)
|
||||
apply_provider_request_routing_policy_to_decision(&input, &mut decision, None)
|
||||
.expect("provider routing mutation should apply");
|
||||
|
||||
assert_eq!(
|
||||
@@ -1035,6 +1269,179 @@ mod tests {
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn codex_compact_contract_is_terminal_after_routing_mutations() {
|
||||
let mut input = sample_decision_input();
|
||||
input
|
||||
.routing_context
|
||||
.as_mut()
|
||||
.expect("routing context")
|
||||
.client_api_format = "openai:responses:compact".to_string();
|
||||
set_provider_request_rules(
|
||||
&mut input,
|
||||
&["gpt-5"],
|
||||
json!([
|
||||
{
|
||||
"type": "json_patch_body",
|
||||
"patch": [
|
||||
{"op": "add", "path": "/store", "value": true},
|
||||
{"op": "add", "path": "/top_logprobs", "value": 5},
|
||||
{"op": "add", "path": "/custom_extension", "value": true},
|
||||
{"op": "replace", "path": "/input", "value": "routed compact input"},
|
||||
{"op": "replace", "path": "/tools", "value": [{
|
||||
"type": "function",
|
||||
"name": "lookup",
|
||||
"cache_control": {"type": "ephemeral"}
|
||||
}]}
|
||||
]
|
||||
},
|
||||
{
|
||||
"type": "patch_headers",
|
||||
"patch": [
|
||||
{"op": "set", "name": "chatgpt-account-id", "value": "spoofed"},
|
||||
{"op": "set", "name": "x-openai-fedramp", "value": "false"},
|
||||
{"op": "set", "name": "x-client-request-id", "value": "spoofed"},
|
||||
{"op": "set", "name": "accept", "value": "text/event-stream"},
|
||||
{"op": "set", "name": "content-encoding", "value": "zstd"}
|
||||
]
|
||||
}
|
||||
]),
|
||||
);
|
||||
let mut decision = sample_decision();
|
||||
decision.provider_type = Some("codex".to_string());
|
||||
decision.provider_api_format = Some("openai:responses:compact".to_string());
|
||||
decision.client_api_format = Some("openai:responses:compact".to_string());
|
||||
decision.provider_request_body = Some(json!({
|
||||
"model": "gpt-5",
|
||||
"input": [],
|
||||
"tools": [{"type": "function", "name": "lookup"}]
|
||||
}));
|
||||
decision
|
||||
.provider_request_headers
|
||||
.insert(CODEX_ACCOUNT_ID_HEADER.to_string(), "account-1".to_string());
|
||||
decision
|
||||
.provider_request_headers
|
||||
.insert(CODEX_FEDRAMP_HEADER.to_string(), "true".to_string());
|
||||
|
||||
apply_provider_request_routing_policy_to_decision(&input, &mut decision, None)
|
||||
.expect("terminal contract should accept the projected request");
|
||||
|
||||
let body = decision.provider_request_body.as_ref().expect("body");
|
||||
assert_eq!(body["parallel_tool_calls"], false);
|
||||
assert_eq!(body["input"][0]["type"], "message");
|
||||
assert_eq!(
|
||||
body["input"][0]["content"][0]["text"],
|
||||
"routed compact input"
|
||||
);
|
||||
assert!(body["tools"][0].get("cache_control").is_none());
|
||||
for field in ["store", "top_logprobs", "custom_extension"] {
|
||||
assert!(
|
||||
body.get(field).is_none(),
|
||||
"unexpected Compact field: {field}"
|
||||
);
|
||||
}
|
||||
assert_eq!(
|
||||
decision
|
||||
.provider_request_headers
|
||||
.get(CODEX_ACCOUNT_ID_HEADER),
|
||||
Some(&"account-1".to_string())
|
||||
);
|
||||
assert_eq!(
|
||||
decision.provider_request_headers.get(CODEX_FEDRAMP_HEADER),
|
||||
Some(&"true".to_string())
|
||||
);
|
||||
for header in ["x-client-request-id", "accept", "content-encoding"] {
|
||||
assert!(
|
||||
!decision.provider_request_headers.contains_key(header),
|
||||
"unexpected Compact header: {header}"
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn codex_responses_lite_contract_is_terminal_after_routing_mutations() {
|
||||
let mut input = sample_decision_input();
|
||||
input.requested_model = "gpt-future-agent".to_string();
|
||||
input
|
||||
.routing_context
|
||||
.as_mut()
|
||||
.expect("routing context")
|
||||
.client_api_format = "openai:responses:compact".to_string();
|
||||
set_provider_request_rules(
|
||||
&mut input,
|
||||
&["gpt-future-agent"],
|
||||
json!([
|
||||
{
|
||||
"type": "json_patch_body",
|
||||
"patch": [
|
||||
{"op": "replace", "path": "/input", "value": "routed compact input"},
|
||||
{"op": "add", "path": "/instructions", "value": "Routed instructions"},
|
||||
{"op": "replace", "path": "/tools", "value": [{
|
||||
"type": "function",
|
||||
"name": "lookup",
|
||||
"parameters": {},
|
||||
"cache_control": {"type": "ephemeral"}
|
||||
}]},
|
||||
{"op": "add", "path": "/parallel_tool_calls", "value": true},
|
||||
{"op": "add", "path": "/reasoning", "value": {
|
||||
"effort": "high",
|
||||
"context": "current_turn"
|
||||
}}
|
||||
]
|
||||
},
|
||||
{
|
||||
"type": "patch_headers",
|
||||
"patch": [{
|
||||
"op": "set",
|
||||
"name": "x-openai-internal-codex-responses-lite",
|
||||
"value": "false"
|
||||
}]
|
||||
}
|
||||
]),
|
||||
);
|
||||
let mut decision = sample_decision();
|
||||
decision.provider_type = Some("codex".to_string());
|
||||
decision.provider_api_format = Some("openai:responses:compact".to_string());
|
||||
decision.client_api_format = Some("openai:responses:compact".to_string());
|
||||
decision.mapped_model = Some("gpt-future-agent".to_string());
|
||||
decision.provider_request_body = Some(json!({
|
||||
"model": "gpt-future-agent",
|
||||
"input": [],
|
||||
"tools": []
|
||||
}));
|
||||
let transport = sample_codex_transport_with_card();
|
||||
|
||||
apply_provider_request_routing_policy_to_decision(&input, &mut decision, Some(&transport))
|
||||
.expect("terminal Lite contract should accept the projected request");
|
||||
|
||||
let body = decision.provider_request_body.as_ref().expect("body");
|
||||
assert_eq!(body["input"][0]["type"], "additional_tools");
|
||||
assert_eq!(body["input"][0]["tools"][0]["name"], "lookup");
|
||||
assert!(body["input"][0]["tools"][0].get("cache_control").is_none());
|
||||
assert_eq!(body["input"][1]["role"], "developer");
|
||||
assert_eq!(
|
||||
body["input"][1]["content"][0]["text"],
|
||||
"Routed instructions"
|
||||
);
|
||||
assert_eq!(body["input"][2]["role"], "user");
|
||||
assert_eq!(
|
||||
body["input"][2]["content"][0]["text"],
|
||||
"routed compact input"
|
||||
);
|
||||
assert!(body.get("tools").is_none());
|
||||
assert!(body.get("instructions").is_none());
|
||||
assert_eq!(body["parallel_tool_calls"], false);
|
||||
assert_eq!(body["reasoning"]["effort"], "high");
|
||||
assert_eq!(body["reasoning"]["context"], "all_turns");
|
||||
assert_eq!(
|
||||
decision
|
||||
.provider_request_headers
|
||||
.get(CODEX_RESPONSES_LITE_HEADER)
|
||||
.map(String::as_str),
|
||||
Some("true")
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn provider_request_routing_policy_rejects_body_patch_without_json_body() {
|
||||
let input = sample_decision_input();
|
||||
@@ -1042,7 +1449,7 @@ mod tests {
|
||||
decision.provider_request_body = None;
|
||||
decision.provider_request_body_base64 = Some("AA==".to_string());
|
||||
|
||||
let error = apply_provider_request_routing_policy_to_decision(&input, &mut decision)
|
||||
let error = apply_provider_request_routing_policy_to_decision(&input, &mut decision, None)
|
||||
.expect_err("provider body patch should reject binary upstream bodies");
|
||||
|
||||
match error {
|
||||
@@ -1067,6 +1474,7 @@ mod tests {
|
||||
let mut input = sample_decision_input();
|
||||
set_provider_request_rules(
|
||||
&mut input,
|
||||
&["gpt-5"],
|
||||
json!([{
|
||||
"type": "patch_headers",
|
||||
"patch": [{
|
||||
@@ -1080,7 +1488,7 @@ mod tests {
|
||||
decision.provider_request_body = None;
|
||||
decision.provider_request_body_base64 = Some("AA==".to_string());
|
||||
|
||||
apply_provider_request_routing_policy_to_decision(&input, &mut decision)
|
||||
apply_provider_request_routing_policy_to_decision(&input, &mut decision, None)
|
||||
.expect("header-only provider routing mutation should apply without JSON body");
|
||||
|
||||
assert_eq!(decision.provider_request_body, None);
|
||||
@@ -1112,7 +1520,7 @@ mod tests {
|
||||
"priority_slot": 3
|
||||
}));
|
||||
|
||||
apply_provider_request_routing_policy_to_decision(&input, &mut decision)
|
||||
apply_provider_request_routing_policy_to_decision(&input, &mut decision, None)
|
||||
.expect("provider routing mutation should seed pool trace");
|
||||
|
||||
let routing_trace = &decision.report_context.as_ref().unwrap()["routing_trace"];
|
||||
|
||||
Reference in New Issue
Block a user