Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 2 additions & 1 deletion crates/service/src/aggregate_api.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1275,7 +1275,8 @@ fn build_codex_probe_body(model: &str) -> serde_json::Value {
"text": "Who are you?"
}]
}],
"stream": true
"stream": true,
"store": false
})
}

Expand Down
1 change: 1 addition & 0 deletions crates/service/src/aggregate_api_tests.rs
Original file line number Diff line number Diff line change
Expand Up @@ -443,6 +443,7 @@ fn codex_responses_probe_uses_valid_input_text_content() {
let body: Value = serde_json::from_str(captured.1.as_str()).expect("parse body");
assert_eq!(body["model"], "gpt-5.6-sol");
assert_eq!(body["input"][0]["content"][0]["type"], "input_text");
assert_eq!(body["store"], false);
}

#[test]
Expand Down
3 changes: 2 additions & 1 deletion crates/service/src/gateway/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -165,7 +165,8 @@ pub(crate) use request_log::{
#[cfg(test)]
use request_rewrite::apply_request_overrides_with_service_tier_and_prompt_cache_key;
use request_rewrite::{
apply_codex_candidate_transport_rules, apply_request_overrides_for_deferred_aggregate,
apply_codex_candidate_transport_rules, apply_external_dynamic_tools_transport_rules,
apply_request_overrides_for_deferred_aggregate,
apply_request_overrides_with_service_tier_and_forced_prompt_cache_key_scope,
apply_request_overrides_with_service_tier_and_prompt_cache_key_scope, compute_upstream_url,
};
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -423,7 +423,7 @@ fn normalize_codex_backend_service_tier(path: &str, obj: &mut Map<String, Value>
normalize_service_tier(path, obj)
}

fn normalize_dynamic_tools_to_tools(path: &str, obj: &mut Map<String, Value>) -> bool {
pub(crate) fn normalize_dynamic_tools_to_tools(path: &str, obj: &mut Map<String, Value>) -> bool {
if !is_responses_path(path) {
return false;
}
Expand Down
28 changes: 28 additions & 0 deletions crates/service/src/gateway/request/request_rewrite.rs
Original file line number Diff line number Diff line change
Expand Up @@ -571,6 +571,22 @@ pub(super) fn apply_codex_candidate_transport_rules(path: &str, body: Vec<u8>) -
serde_json::to_vec(&payload).unwrap_or(body)
}

pub(super) fn apply_external_dynamic_tools_transport_rules(path: &str, body: Vec<u8>) -> Vec<u8> {
if body.is_empty() || !responses::is_responses_path(path) {
return body;
}
let Ok(mut payload) = serde_json::from_slice::<Value>(&body) else {
return body;
};
let Some(obj) = payload.as_object_mut() else {
return body;
};
if !responses::normalize_dynamic_tools_to_tools(path, obj) {
return body;
}
serde_json::to_vec(&payload).unwrap_or(body)
}

/// 函数 `apply_request_overrides_with_prompt_cache_key_mode`
///
/// 作者: gaohongshun
Expand Down Expand Up @@ -604,6 +620,12 @@ fn apply_request_overrides_with_prompt_cache_key_mode(
let use_codex_responses_compat =
should_apply_codex_responses_compat(path, upstream_base_url, resolve_default_upstream_base);
let use_codex_compat_rewrite = allow_codex_compat_rewrite && use_codex_responses_compat;
// Non-official Responses providers commonly reject Codex's dynamicTools
// envelope. Convert it only when an explicit non-Codex upstream is known;
// deferred routing must keep the original body until a candidate is chosen.
let normalize_external_dynamic_tools = upstream_base_url.is_some()
&& responses::is_responses_path(path)
&& !use_codex_responses_compat;
let chat_rules_path = chat_request_rules_path(path);
let normalized_model = model_slug
.map(str::trim)
Expand All @@ -624,6 +646,12 @@ fn apply_request_overrides_with_prompt_cache_key_mode(
let mut changed = false;
let mut dropped_keys = Vec::new();

if normalize_external_dynamic_tools
&& responses::normalize_dynamic_tools_to_tools(path, obj)
{
changed = true;
}

// Ultra 由 Codex 客户端负责多代理编排;单个上游请求必须使用 Max。
// 客户端原始值在进入此重写前已单独采集,供请求日志展示 ultra -> max。
if normalize_client_ultra_for_upstream(obj) {
Expand Down
33 changes: 33 additions & 0 deletions crates/service/src/gateway/request/tests/request_rewrite_tests.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1382,6 +1382,39 @@ fn responses_dynamic_tools_are_mapped_to_tools_for_codex_backend() {
);
}

#[test]
fn responses_dynamic_tools_are_mapped_for_explicit_non_codex_upstream() {
let _guard = crate::test_env_guard();
let body = json!({
"model": "muse-spark-1.2-contributor",
"input": "hello",
"dynamicTools": [{
"name": "spawn_agent",
"description": "start a subagent",
"input_schema": {
"type": "object",
"properties": {"prompt": {"type": "string"}}
}
}]
});
let out = apply_request_overrides(
"/v1/responses",
serde_json::to_vec(&body).expect("serialize request body"),
None,
None,
Some("https://muse.example/v1"),
);
let value: serde_json::Value = serde_json::from_slice(&out).expect("parse output body");
let tools = value
.get("tools")
.and_then(serde_json::Value::as_array)
.expect("tools array");
assert_eq!(tools.len(), 1);
assert_eq!(tools[0]["type"], "function");
assert_eq!(tools[0]["name"], "spawn_agent");
assert!(value.get("dynamicTools").is_none());
}

/// 函数 `responses_preserves_priority_service_tier_for_codex_backend`
///
/// 作者: gaohongshun
Expand Down
13 changes: 12 additions & 1 deletion crates/service/src/gateway/upstream/protocol/aggregate_api.rs
Original file line number Diff line number Diff line change
Expand Up @@ -157,6 +157,14 @@ fn rewrite_body_for_candidate_transport(
upstream_url: &str,
) -> Bytes {
let rewritten = rewrite_body_model_override(body, candidate.model_override.as_deref());
if !super::super::config::should_send_chatgpt_account_header(upstream_url) {
return Bytes::from(
super::super::super::apply_external_dynamic_tools_transport_rules(
path,
rewritten.to_vec(),
),
);
}
if normalize_provider_type_value(candidate.provider_type.as_str())
== AGGREGATE_API_PROVIDER_CODEX
&& super::super::config::should_send_chatgpt_account_header(upstream_url)
Expand Down Expand Up @@ -1640,7 +1648,7 @@ mod bridge_tests {
fn candidate_transport_rewrite_isolated_between_codex_and_generic_upstreams() {
let _guard = crate::test_env_guard();
let body = Bytes::from_static(
br#"{"model":"platform-model","input":"hello","stream":false,"service_tier":"fast"}"#,
br#"{"model":"platform-model","input":"hello","stream":false,"service_tier":"fast","dynamicTools":[{"name":"spawn_agent"}]}"#,
);
let mut codex = candidate("codex", 0);
codex.model_override = Some("gpt-5.4".to_string());
Expand Down Expand Up @@ -1696,6 +1704,9 @@ mod bridge_tests {
assert_eq!(generic_value["input"], "hello");
assert_eq!(generic_value["stream"], false);
assert_eq!(generic_value["service_tier"], "fast");
assert_eq!(generic_value["tools"][0]["type"], "function");
assert_eq!(generic_value["tools"][0]["name"], "spawn_agent");
assert!(generic_value.get("dynamicTools").is_none());
assert!(generic_value.get("instructions").is_none());
assert!(generic_value.get("store").is_none());
assert!(generic_value.get("tool_choice").is_none());
Expand Down