migrate ai format conversion to responses adapters

This commit is contained in:
fawney19
2026-04-26 20:32:55 +08:00
parent e36fb8c07a
commit 5b914aa78c
183 changed files with 13539 additions and 3401 deletions

12
Cargo.lock generated
View File

@@ -40,10 +40,21 @@ dependencies = [
"uuid",
]
[[package]]
name = "aether-ai-formats"
version = "0.1.0"
dependencies = [
"regex",
"serde",
"serde_json",
"uuid",
]
[[package]]
name = "aether-ai-pipeline"
version = "0.1.0"
dependencies = [
"aether-ai-formats",
"aether-contracts",
"aether-provider-transport",
"aether-usage-runtime",
@@ -220,6 +231,7 @@ dependencies = [
name = "aether-provider-transport"
version = "0.1.0"
dependencies = [
"aether-ai-formats",
"aether-contracts",
"aether-crypto",
"aether-data",

View File

@@ -1,6 +1,7 @@
[workspace]
members = [
"apps/aether-proxy",
"crates/aether-ai-formats",
"crates/aether-admin",
"crates/aether-ai-pipeline",
"crates/aether-data-contracts",
@@ -29,6 +30,7 @@ repository = "https://github.com/fawney19/Aether.git"
[workspace.dependencies]
aether-admin = { path = "crates/aether-admin" }
aether-ai-formats = { path = "crates/aether-ai-formats" }
aether-ai-pipeline = { path = "crates/aether-ai-pipeline" }
aether-data-contracts = { path = "crates/aether-data-contracts" }
aether-cache = { path = "crates/aether-cache" }

View File

@@ -31,7 +31,14 @@ pub(crate) use crate::ai_pipeline::{
OPENAI_COMPACT_SYNC_ERROR_REPORT_KIND, OPENAI_COMPACT_SYNC_FINALIZE_REPORT_KIND,
OPENAI_COMPACT_SYNC_PLAN_KIND, OPENAI_IMAGE_STREAM_PLAN_KIND,
OPENAI_IMAGE_STREAM_SUCCESS_REPORT_KIND, OPENAI_IMAGE_SYNC_FINALIZE_REPORT_KIND,
OPENAI_IMAGE_SYNC_PLAN_KIND, OPENAI_VIDEO_CANCEL_SYNC_PLAN_KIND,
OPENAI_IMAGE_SYNC_PLAN_KIND, OPENAI_RESPONSES_COMPACT_STREAM_PLAN_KIND,
OPENAI_RESPONSES_COMPACT_STREAM_SUCCESS_REPORT_KIND,
OPENAI_RESPONSES_COMPACT_SYNC_ERROR_REPORT_KIND,
OPENAI_RESPONSES_COMPACT_SYNC_FINALIZE_REPORT_KIND, OPENAI_RESPONSES_COMPACT_SYNC_PLAN_KIND,
OPENAI_RESPONSES_COMPACT_SYNC_SUCCESS_REPORT_KIND, OPENAI_RESPONSES_STREAM_PLAN_KIND,
OPENAI_RESPONSES_STREAM_SUCCESS_REPORT_KIND, OPENAI_RESPONSES_SYNC_ERROR_REPORT_KIND,
OPENAI_RESPONSES_SYNC_FINALIZE_REPORT_KIND, OPENAI_RESPONSES_SYNC_PLAN_KIND,
OPENAI_RESPONSES_SYNC_SUCCESS_REPORT_KIND, OPENAI_VIDEO_CANCEL_SYNC_PLAN_KIND,
OPENAI_VIDEO_CONTENT_PLAN_KIND, OPENAI_VIDEO_CREATE_SYNC_FINALIZE_REPORT_KIND,
OPENAI_VIDEO_CREATE_SYNC_PLAN_KIND, OPENAI_VIDEO_DELETE_SYNC_PLAN_KIND,
OPENAI_VIDEO_REMIX_SYNC_PLAN_KIND,

View File

@@ -27,7 +27,7 @@ mod tests {
fn request_conversion_registry_supports_bidirectional_standard_matrix() {
assert_eq!(
request_conversion_kind("openai:chat", "openai:cli"),
Some(RequestConversionKind::ToOpenAIFamilyCli)
Some(RequestConversionKind::ToOpenAiResponses)
);
assert_eq!(
request_conversion_kind("openai:chat", "claude:cli"),
@@ -76,11 +76,11 @@ mod tests {
);
assert_eq!(
sync_cli_response_conversion_kind("claude:chat", "openai:cli"),
Some(SyncCliResponseConversionKind::ToOpenAIFamilyCli)
Some(SyncCliResponseConversionKind::ToOpenAiResponses)
);
assert_eq!(
sync_cli_response_conversion_kind("claude:cli", "openai:compact"),
Some(SyncCliResponseConversionKind::ToOpenAIFamilyCli)
Some(SyncCliResponseConversionKind::ToOpenAiResponses)
);
assert_eq!(
sync_cli_response_conversion_kind("openai:compact", "claude:cli"),
@@ -100,6 +100,7 @@ mod tests {
"openai:chat",
"claude:chat",
"gemini:chat",
"openai:responses",
"openai:cli",
"claude:cli",
"gemini:cli",
@@ -108,6 +109,19 @@ mod tests {
assert_eq!(
request_candidate_api_formats("openai:cli", false),
vec![
"openai:responses",
"openai:cli",
"claude:cli",
"gemini:cli",
"openai:chat",
"claude:chat",
"gemini:chat",
]
);
assert_eq!(
request_candidate_api_formats("openai:responses", false),
vec![
"openai:responses",
"openai:cli",
"claude:cli",
"gemini:cli",
@@ -120,6 +134,7 @@ mod tests {
request_candidate_api_formats("claude:cli", false),
vec![
"claude:cli",
"openai:responses",
"openai:cli",
"gemini:cli",
"claude:chat",
@@ -129,7 +144,7 @@ mod tests {
);
assert_eq!(
request_candidate_api_formats("openai:compact", false),
vec!["openai:compact"]
vec!["openai:responses:compact", "openai:compact"]
);
}

View File

@@ -15,11 +15,11 @@ pub(crate) use crate::ai_pipeline::finalize::standard::{
};
pub(crate) use crate::ai_pipeline::{
aggregate_claude_stream_sync_response, aggregate_gemini_stream_sync_response,
aggregate_openai_chat_stream_sync_response, aggregate_openai_cli_stream_sync_response,
aggregate_openai_chat_stream_sync_response, aggregate_openai_responses_stream_sync_response,
};
pub(crate) use crate::ai_pipeline::{
convert_claude_chat_response_to_openai_chat, convert_claude_cli_response_to_openai_cli,
convert_gemini_chat_response_to_openai_chat, convert_gemini_cli_response_to_openai_cli,
convert_claude_chat_response_to_openai_chat, convert_claude_response_to_openai_responses,
convert_gemini_chat_response_to_openai_chat, convert_gemini_response_to_openai_responses,
};
pub(crate) fn maybe_build_local_core_sync_finalize_response(

View File

@@ -3,9 +3,9 @@ use serde_json::{json, Value};
use crate::ai_pipeline::finalize::sse::encode_json_sse;
use crate::ai_pipeline::{
convert_claude_cli_response_to_openai_cli, convert_gemini_cli_response_to_openai_cli,
convert_openai_chat_response_to_openai_cli, ClaudeClientEmitter, GeminiClientEmitter,
OpenAIChatClientEmitter, OpenAICliClientEmitter, OpenAICliProviderState,
convert_claude_response_to_openai_responses, convert_gemini_response_to_openai_responses,
convert_openai_chat_response_to_openai_responses, ClaudeClientEmitter, GeminiClientEmitter,
OpenAIChatClientEmitter, OpenAIResponsesClientEmitter, OpenAIResponsesProviderState,
};
use crate::GatewayError;
@@ -36,16 +36,19 @@ pub(crate) fn maybe_bridge_standard_sync_json_to_stream(
provider_api_format.as_str(),
client_api_format.as_str(),
);
let Some(openai_cli_response) = convert_provider_sync_response_to_openai_cli(
let Some(openai_responses_response) = convert_provider_sync_response_to_openai_responses(
provider_body_json,
provider_api_format.as_str(),
&bridge_context,
) else {
return Ok(None);
};
let terminal_summary = build_terminal_summary_from_openai_cli_response(&openai_cli_response);
let canonical_frames =
build_canonical_frames_from_openai_cli_response(&openai_cli_response, &bridge_context)?;
let terminal_summary =
build_terminal_summary_from_openai_responses_response(&openai_responses_response);
let canonical_frames = build_canonical_frames_from_openai_responses_response(
&openai_responses_response,
&bridge_context,
)?;
let sse_body =
emit_client_stream_from_canonical_frames(canonical_frames, client_api_format.as_str())?;
@@ -100,6 +103,7 @@ fn maybe_bridge_openai_image_sync_json_to_stream(
.map(ToOwned::to_owned)
.or_else(|| image_bridge_model(report_context)),
observed_finish: true,
unknown_event_count: 0,
parser_error: None,
}),
}))
@@ -113,8 +117,10 @@ fn is_standard_api_format(value: &str) -> bool {
matches!(
value,
"openai:chat"
| "openai:responses"
| "openai:cli"
| "openai:compact"
| "openai:responses:compact"
| "claude:chat"
| "claude:cli"
| "gemini:chat"
@@ -194,36 +200,40 @@ fn build_bridge_report_context(
context
}
fn convert_provider_sync_response_to_openai_cli(
fn convert_provider_sync_response_to_openai_responses(
provider_body_json: &Value,
provider_api_format: &str,
report_context: &Value,
) -> Option<Value> {
match provider_api_format {
"openai:cli" | "openai:compact" => Some(provider_body_json.clone()),
"openai:chat" => {
convert_openai_chat_response_to_openai_cli(provider_body_json, report_context, false)
"openai:responses" | "openai:cli" | "openai:compact" | "openai:responses:compact" => {
Some(provider_body_json.clone())
}
"openai:chat" => convert_openai_chat_response_to_openai_responses(
provider_body_json,
report_context,
false,
),
"claude:chat" | "claude:cli" => {
convert_claude_cli_response_to_openai_cli(provider_body_json, report_context)
convert_claude_response_to_openai_responses(provider_body_json, report_context)
}
"gemini:chat" | "gemini:cli" => {
convert_gemini_cli_response_to_openai_cli(provider_body_json, report_context)
convert_gemini_response_to_openai_responses(provider_body_json, report_context)
}
_ => None,
}
}
fn build_canonical_frames_from_openai_cli_response(
openai_cli_response: &Value,
fn build_canonical_frames_from_openai_responses_response(
openai_responses_response: &Value,
report_context: &Value,
) -> Result<Vec<crate::ai_pipeline::CanonicalStreamFrame>, GatewayError> {
let mut state = OpenAICliProviderState::default();
let mut state = OpenAIResponsesProviderState::default();
let line = format!(
"data: {}\n",
serde_json::to_string(&json!({
"type": "response.completed",
"response": openai_cli_response,
"response": openai_responses_response,
}))
.map_err(|err| GatewayError::Internal(err.to_string()))?
);
@@ -247,9 +257,9 @@ fn emit_client_stream_from_canonical_frames(
let mut emitter = OpenAIChatClientEmitter::default();
emit_with_openai_chat_emitter(&mut emitter, canonical_frames)
}
"openai:cli" | "openai:compact" => {
let mut emitter = OpenAICliClientEmitter::default();
emit_with_openai_cli_emitter(&mut emitter, canonical_frames)
"openai:responses" | "openai:cli" | "openai:compact" | "openai:responses:compact" => {
let mut emitter = OpenAIResponsesClientEmitter::default();
emit_with_openai_responses_emitter(&mut emitter, canonical_frames)
}
"claude:chat" | "claude:cli" => {
let mut emitter = ClaudeClientEmitter::default();
@@ -283,8 +293,8 @@ fn emit_with_openai_chat_emitter(
Ok(output)
}
fn emit_with_openai_cli_emitter(
emitter: &mut OpenAICliClientEmitter,
fn emit_with_openai_responses_emitter(
emitter: &mut OpenAIResponsesClientEmitter,
canonical_frames: Vec<crate::ai_pipeline::CanonicalStreamFrame>,
) -> Result<Vec<u8>, GatewayError> {
let mut output = Vec::new();
@@ -343,10 +353,10 @@ fn emit_with_gemini_emitter(
Ok(output)
}
fn build_terminal_summary_from_openai_cli_response(
openai_cli_response: &Value,
fn build_terminal_summary_from_openai_responses_response(
openai_responses_response: &Value,
) -> Option<ExecutionStreamTerminalSummary> {
let response = openai_cli_response.as_object()?;
let response = openai_responses_response.as_object()?;
let response_id = response
.get("id")
.and_then(Value::as_str)
@@ -358,7 +368,7 @@ fn build_terminal_summary_from_openai_cli_response(
let finish_reason = response
.get("output")
.and_then(Value::as_array)
.map(|output| resolve_openai_cli_finish_reason(output))
.map(|output| resolve_openai_responses_finish_reason(output))
.filter(|value| !value.trim().is_empty());
let standardized_usage = response
.get("usage")
@@ -369,11 +379,12 @@ fn build_terminal_summary_from_openai_cli_response(
response_id,
model,
observed_finish: true,
unknown_event_count: 0,
parser_error: None,
})
}
fn resolve_openai_cli_finish_reason(output: &[Value]) -> String {
fn resolve_openai_responses_finish_reason(output: &[Value]) -> String {
let has_tool_calls = output.iter().filter_map(Value::as_object).any(|item| {
item.get("type")
.and_then(Value::as_str)

View File

@@ -5,15 +5,15 @@ mod stream;
pub(crate) use crate::ai_pipeline::{
aggregate_standard_chat_stream_sync_response, aggregate_standard_cli_stream_sync_response,
build_openai_cli_response, convert_claude_chat_response_to_openai_chat,
convert_claude_cli_response_to_openai_cli, convert_gemini_chat_response_to_openai_chat,
convert_gemini_cli_response_to_openai_cli, convert_openai_chat_response_to_claude_chat,
convert_openai_chat_response_to_gemini_chat, convert_openai_chat_response_to_openai_cli,
convert_openai_cli_response_to_openai_chat, convert_standard_chat_response,
build_openai_responses_response, convert_claude_chat_response_to_openai_chat,
convert_claude_response_to_openai_responses, convert_gemini_chat_response_to_openai_chat,
convert_gemini_response_to_openai_responses, convert_openai_chat_response_to_claude_chat,
convert_openai_chat_response_to_gemini_chat, convert_openai_chat_response_to_openai_responses,
convert_openai_responses_response_to_openai_chat, convert_standard_chat_response,
convert_standard_cli_response,
maybe_build_openai_chat_cross_format_sync_product_from_normalized_payload,
maybe_build_openai_cli_cross_format_sync_product_from_normalized_payload,
maybe_build_openai_cli_same_family_sync_body_from_normalized_payload,
maybe_build_openai_responses_cross_format_sync_product_from_normalized_payload,
maybe_build_openai_responses_same_family_sync_body_from_normalized_payload,
maybe_build_standard_cross_format_sync_product,
maybe_build_standard_cross_format_sync_product_from_normalized_payload,
maybe_build_standard_same_format_sync_body_from_normalized_payload,

View File

@@ -59,7 +59,7 @@ fn standard_sync_bridge_converts_openai_chat_sync_json_to_openai_chat_sse() {
}
#[test]
fn standard_sync_bridge_converts_claude_sync_json_to_openai_cli_sse() {
fn standard_sync_bridge_converts_claude_sync_json_to_openai_responses_sse() {
let report_context = json!({
"mapped_model": "claude-sonnet-4-5",
});
@@ -80,7 +80,7 @@ fn standard_sync_bridge_converts_claude_sync_json_to_openai_cli_sse() {
}
}),
"claude:chat",
"openai:cli",
"openai:responses",
Some(&report_context),
)
.expect("bridge should succeed")
@@ -327,10 +327,10 @@ fn gemini_cli_v1internal_stream_rewriter_unwraps_response_object() {
}
#[test]
fn openai_chat_error_to_openai_cli_stream_rewriter_converts_to_response_failed() {
fn openai_chat_error_to_openai_responses_stream_rewriter_converts_to_response_failed() {
let report_context = json!({
"provider_api_format": "openai:chat",
"client_api_format": "openai:cli",
"client_api_format": "openai:responses",
"needs_conversion": true,
"mapped_model": "gpt-5.4",
});
@@ -351,10 +351,10 @@ fn openai_chat_error_to_openai_cli_stream_rewriter_converts_to_response_failed()
}
#[test]
fn claude_error_to_openai_cli_stream_rewriter_converts_to_response_failed() {
fn claude_error_to_openai_responses_stream_rewriter_converts_to_response_failed() {
let report_context = json!({
"provider_api_format": "claude:chat",
"client_api_format": "openai:cli",
"client_api_format": "openai:responses",
"needs_conversion": true,
"mapped_model": "claude-sonnet-4-5",
});
@@ -379,10 +379,10 @@ fn claude_error_to_openai_cli_stream_rewriter_converts_to_response_failed() {
}
#[test]
fn gemini_error_to_openai_cli_stream_rewriter_converts_to_response_failed() {
fn gemini_error_to_openai_responses_stream_rewriter_converts_to_response_failed() {
let report_context = json!({
"provider_api_format": "gemini:cli",
"client_api_format": "openai:cli",
"client_api_format": "openai:responses",
"needs_conversion": true,
"mapped_model": "gemini-2.5-pro",
});
@@ -584,9 +584,9 @@ fn gemini_to_openai_chat_stream_rewriter_buffers_and_converts_function_call() {
}
#[test]
fn openai_cli_to_openai_chat_stream_rewriter_converts_text_deltas_immediately() {
fn openai_responses_to_openai_chat_stream_rewriter_converts_text_deltas_immediately() {
let report_context = json!({
"provider_api_format": "openai:cli",
"provider_api_format": "openai:responses",
"client_api_format": "openai:chat",
"needs_conversion": true,
"mapped_model": "gpt-5.4",
@@ -653,9 +653,9 @@ fn openai_cli_to_openai_chat_stream_rewriter_converts_text_deltas_immediately()
}
#[test]
fn openai_cli_to_openai_chat_stream_rewriter_converts_completed_event_without_buffering() {
fn openai_responses_to_openai_chat_stream_rewriter_converts_completed_event_without_buffering() {
let report_context = json!({
"provider_api_format": "openai:cli",
"provider_api_format": "openai:responses",
"client_api_format": "openai:chat",
"needs_conversion": true,
"mapped_model": "gpt-5.4",
@@ -715,10 +715,10 @@ fn antigravity_gemini_to_openai_chat_stream_rewriter_unwraps_and_converts_functi
}
#[test]
fn antigravity_gemini_to_openai_cli_stream_rewriter_unwraps_and_converts_function_call() {
fn antigravity_gemini_to_openai_responses_stream_rewriter_unwraps_and_converts_function_call() {
let report_context = json!({
"provider_api_format": "gemini:cli",
"client_api_format": "openai:cli",
"client_api_format": "openai:responses",
"needs_conversion": true,
"envelope_name": "antigravity:v1internal",
"mapped_model": "claude-sonnet-4-5",
@@ -742,10 +742,10 @@ fn antigravity_gemini_to_openai_cli_stream_rewriter_unwraps_and_converts_functio
}
#[test]
fn gemini_to_openai_cli_stream_rewriter_buffers_and_converts_to_completed_event() {
fn gemini_to_openai_responses_stream_rewriter_buffers_and_converts_to_completed_event() {
let report_context = json!({
"provider_api_format": "gemini:cli",
"client_api_format": "openai:cli",
"client_api_format": "openai:responses",
"needs_conversion": true,
"mapped_model": "gemini-2.5-pro",
});
@@ -777,10 +777,10 @@ fn gemini_to_openai_cli_stream_rewriter_buffers_and_converts_to_completed_event(
}
#[test]
fn claude_to_openai_cli_stream_rewriter_buffers_and_converts_to_completed_event() {
fn claude_to_openai_responses_stream_rewriter_buffers_and_converts_to_completed_event() {
let report_context = json!({
"provider_api_format": "claude:cli",
"client_api_format": "openai:cli",
"client_api_format": "openai:responses",
"needs_conversion": true,
"mapped_model": "claude-sonnet-4-5",
});
@@ -815,10 +815,10 @@ fn claude_to_openai_cli_stream_rewriter_buffers_and_converts_to_completed_event(
}
#[test]
fn claude_to_openai_cli_stream_rewriter_converts_tool_use_to_function_call() {
fn claude_to_openai_responses_stream_rewriter_converts_tool_use_to_function_call() {
let report_context = json!({
"provider_api_format": "claude:cli",
"client_api_format": "openai:cli",
"client_api_format": "openai:responses",
"needs_conversion": true,
"mapped_model": "claude-sonnet-4-5",
});
@@ -861,10 +861,10 @@ fn claude_to_openai_cli_stream_rewriter_converts_tool_use_to_function_call() {
}
#[test]
fn gemini_to_openai_cli_stream_rewriter_converts_function_call_to_completed_event() {
fn gemini_to_openai_responses_stream_rewriter_converts_function_call_to_completed_event() {
let report_context = json!({
"provider_api_format": "gemini:cli",
"client_api_format": "openai:cli",
"client_api_format": "openai:responses",
"needs_conversion": true,
"mapped_model": "gemini-2.5-pro",
});
@@ -891,7 +891,7 @@ fn gemini_to_openai_cli_stream_rewriter_converts_function_call_to_completed_even
fn gemini_to_openai_compact_stream_rewriter_converts_function_call_to_completed_event() {
let report_context = json!({
"provider_api_format": "gemini:cli",
"client_api_format": "openai:compact",
"client_api_format": "openai:responses:compact",
"needs_conversion": true,
"mapped_model": "gemini-2.5-pro",
});

View File

@@ -6,14 +6,15 @@ use serde_json::json;
use super::{
aggregate_claude_stream_sync_response, aggregate_gemini_stream_sync_response,
aggregate_openai_chat_stream_sync_response, aggregate_openai_cli_stream_sync_response,
convert_claude_chat_response_to_openai_chat, convert_claude_cli_response_to_openai_cli,
convert_gemini_chat_response_to_openai_chat, convert_gemini_cli_response_to_openai_cli,
aggregate_openai_chat_stream_sync_response, aggregate_openai_responses_stream_sync_response,
convert_claude_chat_response_to_openai_chat, convert_claude_response_to_openai_responses,
convert_gemini_chat_response_to_openai_chat, convert_gemini_response_to_openai_responses,
maybe_build_local_core_sync_finalize_response,
};
use crate::ai_pipeline::GatewayControlDecision;
use crate::ai_pipeline::{
convert_openai_chat_response_to_openai_cli, convert_openai_cli_response_to_openai_chat,
convert_openai_chat_response_to_openai_responses,
convert_openai_responses_response_to_openai_chat,
};
use crate::usage::GatewaySyncReportRequest;
@@ -24,7 +25,7 @@ fn test_decision() -> GatewayControlDecision {
route_class: Some("ai_public".to_string()),
route_family: Some("openai".to_string()),
route_kind: Some("compact".to_string()),
auth_endpoint_signature: Some("openai:compact".to_string()),
auth_endpoint_signature: Some("openai:responses:compact".to_string()),
execution_runtime_candidate: true,
auth_context: None,
admin_principal: None,
@@ -158,7 +159,7 @@ fn aggregates_openai_chat_stream_text_chunks_to_final_response() {
}
#[test]
fn aggregates_openai_cli_stream_completed_event_to_final_response() {
fn aggregates_openai_responses_stream_completed_event_to_final_response() {
let body = concat!(
"event: response.created\n",
"data: {\"type\":\"response.created\",\"response\":{\"id\":\"resp_123\",\"object\":\"response\",\"model\":\"gpt-5\",\"status\":\"in_progress\",\"output\":[]}}\n\n",
@@ -168,8 +169,8 @@ fn aggregates_openai_cli_stream_completed_event_to_final_response() {
"data: {\"type\":\"response.completed\",\"response\":{\"id\":\"resp_123\",\"object\":\"response\",\"model\":\"gpt-5\",\"status\":\"completed\",\"output\":[],\"usage\":{\"input_tokens\":1,\"output_tokens\":2,\"total_tokens\":3}}}\n\n",
);
let result =
aggregate_openai_cli_stream_sync_response(body.as_bytes()).expect("result should exist");
let result = aggregate_openai_responses_stream_sync_response(body.as_bytes())
.expect("result should exist");
assert_eq!(
result,
@@ -199,7 +200,7 @@ fn aggregates_openai_cli_stream_completed_event_to_final_response() {
}
#[test]
fn aggregates_openai_cli_stream_tool_call_events_to_final_response() {
fn aggregates_openai_responses_stream_tool_call_events_to_final_response() {
let body = concat!(
"event: response.created\n",
"data: {\"type\":\"response.created\",\"response\":{\"id\":\"resp_tool_123\",\"object\":\"response\",\"model\":\"gpt-5\",\"status\":\"in_progress\",\"output\":[]}}\n\n",
@@ -211,8 +212,8 @@ fn aggregates_openai_cli_stream_tool_call_events_to_final_response() {
"data: {\"type\":\"response.completed\",\"response\":{\"id\":\"resp_tool_123\",\"object\":\"response\",\"model\":\"gpt-5\",\"status\":\"completed\",\"output\":[],\"usage\":{\"input_tokens\":1,\"output_tokens\":2,\"total_tokens\":3}}}\n\n",
);
let result =
aggregate_openai_cli_stream_sync_response(body.as_bytes()).expect("result should exist");
let result = aggregate_openai_responses_stream_sync_response(body.as_bytes())
.expect("result should exist");
assert_eq!(
result,
@@ -651,8 +652,8 @@ fn converts_gemini_chat_inline_data_to_openai_chat_image_part() {
}
#[test]
fn converts_openai_cli_reasoning_item_to_openai_chat_reasoning_content() {
let result = convert_openai_cli_response_to_openai_chat(
fn converts_openai_responses_reasoning_item_to_openai_chat_reasoning_content() {
let result = convert_openai_responses_response_to_openai_chat(
&json!({
"id": "resp_reason_123",
"object": "response",
@@ -686,7 +687,7 @@ fn converts_openai_cli_reasoning_item_to_openai_chat_reasoning_content() {
}),
&json!({
"client_api_format": "openai:chat",
"provider_api_format": "openai:cli",
"provider_api_format": "openai:responses",
"model": "gpt-5"
}),
)
@@ -700,8 +701,8 @@ fn converts_openai_cli_reasoning_item_to_openai_chat_reasoning_content() {
}
#[test]
fn converts_openai_cli_output_image_to_openai_chat_image_part() {
let result = convert_openai_cli_response_to_openai_chat(
fn converts_openai_responses_output_image_to_openai_chat_image_part() {
let result = convert_openai_responses_response_to_openai_chat(
&json!({
"id": "resp_img_cli_123",
"object": "response",
@@ -724,7 +725,7 @@ fn converts_openai_cli_output_image_to_openai_chat_image_part() {
}),
&json!({
"client_api_format": "openai:chat",
"provider_api_format": "openai:cli",
"provider_api_format": "openai:responses",
"model": "gpt-5"
}),
)
@@ -742,8 +743,8 @@ fn converts_openai_cli_output_image_to_openai_chat_image_part() {
}
#[test]
fn converts_openai_chat_image_part_to_openai_cli_output_image() {
let result = convert_openai_chat_response_to_openai_cli(
fn converts_openai_chat_image_part_to_openai_responses_output_image() {
let result = convert_openai_chat_response_to_openai_responses(
&json!({
"id": "chatcmpl_img_123",
"object": "chat.completion",
@@ -768,7 +769,7 @@ fn converts_openai_chat_image_part_to_openai_cli_output_image() {
}
}),
&json!({
"client_api_format": "openai:cli",
"client_api_format": "openai:responses",
"provider_api_format": "openai:chat",
"model": "gpt-5"
}),
@@ -786,8 +787,8 @@ fn converts_openai_chat_image_part_to_openai_cli_output_image() {
}
#[test]
fn converts_claude_cli_response_to_openai_cli_response() {
let result = convert_claude_cli_response_to_openai_cli(
fn converts_claude_cli_response_to_openai_responses_response() {
let result = convert_claude_response_to_openai_responses(
&json!({
"id": "msg_cli_123",
"type": "message",
@@ -803,7 +804,7 @@ fn converts_claude_cli_response_to_openai_cli_response() {
}
}),
&json!({
"client_api_format": "openai:cli",
"client_api_format": "openai:responses",
"provider_api_format": "claude:cli",
"model": "gpt-5"
}),
@@ -838,8 +839,8 @@ fn converts_claude_cli_response_to_openai_cli_response() {
}
#[test]
fn converts_claude_cli_tool_use_to_openai_cli_function_call() {
let result = convert_claude_cli_response_to_openai_cli(
fn converts_claude_cli_tool_use_to_openai_responses_function_call() {
let result = convert_claude_response_to_openai_responses(
&json!({
"id": "msg_cli_tool_123",
"type": "message",
@@ -860,7 +861,7 @@ fn converts_claude_cli_tool_use_to_openai_cli_function_call() {
}
}),
&json!({
"client_api_format": "openai:cli",
"client_api_format": "openai:responses",
"provider_api_format": "claude:cli",
"model": "gpt-5"
}),
@@ -904,8 +905,8 @@ fn converts_claude_cli_tool_use_to_openai_cli_function_call() {
}
#[test]
fn converts_gemini_cli_response_to_openai_cli_response() {
let result = convert_gemini_cli_response_to_openai_cli(
fn converts_gemini_cli_response_to_openai_responses_response() {
let result = convert_gemini_response_to_openai_responses(
&json!({
"responseId": "resp_cli_123",
"candidates": [{
@@ -925,7 +926,7 @@ fn converts_gemini_cli_response_to_openai_cli_response() {
}
}),
&json!({
"client_api_format": "openai:cli",
"client_api_format": "openai:responses",
"provider_api_format": "gemini:cli",
"model": "gpt-5"
}),
@@ -963,8 +964,8 @@ fn converts_gemini_cli_response_to_openai_cli_response() {
}
#[test]
fn converts_gemini_cli_function_call_to_openai_cli_function_call() {
let result = convert_gemini_cli_response_to_openai_cli(
fn converts_gemini_cli_function_call_to_openai_responses_function_call() {
let result = convert_gemini_response_to_openai_responses(
&json!({
"responseId": "resp_cli_tool_123",
"candidates": [{
@@ -987,7 +988,7 @@ fn converts_gemini_cli_function_call_to_openai_cli_function_call() {
}
}),
&json!({
"client_api_format": "openai:cli",
"client_api_format": "openai:responses",
"provider_api_format": "gemini:cli",
"model": "gpt-5"
}),
@@ -1034,8 +1035,8 @@ fn converts_gemini_cli_function_call_to_openai_cli_function_call() {
}
#[test]
fn converts_gemini_cli_inline_data_to_openai_cli_output_image() {
let result = convert_gemini_cli_response_to_openai_cli(
fn converts_gemini_cli_inline_data_to_openai_responses_output_image() {
let result = convert_gemini_response_to_openai_responses(
&json!({
"responseId": "resp_cli_img_123",
"candidates": [{
@@ -1060,7 +1061,7 @@ fn converts_gemini_cli_inline_data_to_openai_cli_output_image() {
}
}),
&json!({
"client_api_format": "openai:cli",
"client_api_format": "openai:responses",
"provider_api_format": "gemini:cli",
"model": "gpt-5"
}),
@@ -1082,7 +1083,7 @@ fn local_finalize_handles_openai_compact_cross_format_sync_response() {
trace_id: "trace-compact-sync-123".to_string(),
report_kind: "openai_compact_sync_finalize".to_string(),
report_context: Some(json!({
"client_api_format": "openai:compact",
"client_api_format": "openai:responses:compact",
"provider_api_format": "gemini:cli",
"model": "gpt-5",
"needs_conversion": true,
@@ -1125,7 +1126,7 @@ fn local_finalize_handles_openai_compact_cross_format_sync_response() {
let report = outcome
.background_report
.expect("compact cross-format should downgrade to success report");
assert_eq!(report.report_kind, "openai_cli_sync_success");
assert_eq!(report.report_kind, "openai_responses_sync_success");
assert_eq!(
report.client_body_json.expect("client body should exist")["object"],
"response"
@@ -1138,7 +1139,7 @@ fn local_finalize_handles_openai_compact_cross_format_function_call_response() {
trace_id: "trace-compact-tool-123".to_string(),
report_kind: "openai_compact_sync_finalize".to_string(),
report_context: Some(json!({
"client_api_format": "openai:compact",
"client_api_format": "openai:responses:compact",
"provider_api_format": "gemini:cli",
"model": "gpt-5",
"needs_conversion": true,
@@ -1189,13 +1190,14 @@ fn local_finalize_handles_openai_compact_cross_format_function_call_response() {
}
#[test]
fn local_finalize_handles_openai_cli_openai_family_sync_response_even_when_conversion_flagged() {
fn local_finalize_handles_openai_responses_openai_family_sync_response_even_when_conversion_flagged(
) {
let payload = GatewaySyncReportRequest {
trace_id: "trace-openai-cli-family-conversion-123".to_string(),
report_kind: "openai_cli_sync_finalize".to_string(),
trace_id: "trace-openai-responses-family-conversion-123".to_string(),
report_kind: "openai_responses_sync_finalize".to_string(),
report_context: Some(json!({
"client_api_format": "openai:cli",
"provider_api_format": "openai:compact",
"client_api_format": "openai:responses",
"provider_api_format": "openai:responses:compact",
"model": "gpt-5",
"needs_conversion": true,
"has_envelope": false,
@@ -1230,7 +1232,7 @@ fn local_finalize_handles_openai_cli_openai_family_sync_response_even_when_conve
};
let outcome = maybe_build_local_core_sync_finalize_response(
"trace-openai-cli-family-conversion-123",
"trace-openai-responses-family-conversion-123",
&test_decision(),
&payload,
)
@@ -1241,7 +1243,7 @@ fn local_finalize_handles_openai_cli_openai_family_sync_response_even_when_conve
let report = outcome
.background_report
.expect("same-family finalize should downgrade to success report");
assert_eq!(report.report_kind, "openai_cli_sync_success");
assert_eq!(report.report_kind, "openai_responses_sync_success");
assert_eq!(
report.body_json.expect("provider body should exist")["id"],
"resp_cli_family_123"
@@ -1299,16 +1301,16 @@ fn local_finalize_handles_openai_chat_stream_response_from_openai_chat() {
}
#[test]
fn local_finalize_handles_openai_cli_cross_format_stream_response_from_gemini() {
fn local_finalize_handles_openai_responses_cross_format_stream_response_from_gemini() {
let body = concat!(
"data: {\"responseId\":\"resp_cli_stream_123\",\"candidates\":[{\"content\":{\"parts\":[{\"text\":\"Hello \"}],\"role\":\"model\"},\"index\":0}],\"modelVersion\":\"gemini-2.5-pro-upstream\"}\n\n",
"data: {\"responseId\":\"resp_cli_stream_123\",\"candidates\":[{\"content\":{\"parts\":[{\"text\":\"Hello Gemini CLI\"}],\"role\":\"model\"},\"finishReason\":\"STOP\",\"index\":0}],\"modelVersion\":\"gemini-2.5-pro-upstream\",\"usageMetadata\":{\"promptTokenCount\":2,\"candidatesTokenCount\":3,\"totalTokenCount\":5}}\n\n",
);
let payload = GatewaySyncReportRequest {
trace_id: "trace-openai-cli-xfmt-stream-123".to_string(),
report_kind: "openai_cli_sync_finalize".to_string(),
trace_id: "trace-openai-responses-xfmt-stream-123".to_string(),
report_kind: "openai_responses_sync_finalize".to_string(),
report_context: Some(json!({
"client_api_format": "openai:cli",
"client_api_format": "openai:responses",
"provider_api_format": "gemini:cli",
"model": "gpt-5",
"mapped_model": "gemini-2.5-pro-upstream",
@@ -1324,7 +1326,7 @@ fn local_finalize_handles_openai_cli_cross_format_stream_response_from_gemini()
};
let outcome = maybe_build_local_core_sync_finalize_response(
"trace-openai-cli-xfmt-stream-123",
"trace-openai-responses-xfmt-stream-123",
&test_decision(),
&payload,
)
@@ -1334,7 +1336,7 @@ fn local_finalize_handles_openai_cli_cross_format_stream_response_from_gemini()
let report = outcome
.background_report
.expect("cross-format stream finalize should downgrade to success report");
assert_eq!(report.report_kind, "openai_cli_sync_success");
assert_eq!(report.report_kind, "openai_responses_sync_success");
let client_body = report.client_body_json.expect("client body should exist");
assert_eq!(client_body["object"], "response");
assert_eq!(
@@ -1354,8 +1356,8 @@ fn local_finalize_handles_openai_compact_openai_family_stream_response_even_when
trace_id: "trace-openai-compact-family-stream-123".to_string(),
report_kind: "openai_compact_sync_finalize".to_string(),
report_context: Some(json!({
"client_api_format": "openai:compact",
"provider_api_format": "openai:cli",
"client_api_format": "openai:responses:compact",
"provider_api_format": "openai:responses",
"model": "gpt-5",
"mapped_model": "gpt-5",
"needs_conversion": true,
@@ -1380,7 +1382,7 @@ fn local_finalize_handles_openai_compact_openai_family_stream_response_even_when
let report = outcome
.background_report
.expect("same-family stream finalize should downgrade to success report");
assert_eq!(report.report_kind, "openai_cli_sync_success");
assert_eq!(report.report_kind, "openai_responses_sync_success");
let provider_body = report.body_json.expect("provider body should exist");
assert_eq!(provider_body["object"], "response");
assert_eq!(provider_body["status"], "completed");
@@ -1395,11 +1397,11 @@ fn local_finalize_preserves_provider_stream_body_for_same_format_stream_aggregat
"data: {\"type\":\"response.completed\",\"response\":{\"id\":\"resp_cli_samefmt_stream_123\",\"object\":\"response\",\"model\":\"gpt-5.4\",\"status\":\"completed\",\"output\":[{\"type\":\"message\",\"id\":\"msg_123\",\"role\":\"assistant\",\"status\":\"completed\",\"content\":[{\"type\":\"output_text\",\"text\":\"Hello sync\",\"annotations\":[]}]}],\"usage\":{\"input_tokens\":1,\"output_tokens\":2,\"total_tokens\":3}}}\n\n",
);
let payload = GatewaySyncReportRequest {
trace_id: "trace-openai-cli-samefmt-stream-123".to_string(),
report_kind: "openai_cli_sync_finalize".to_string(),
trace_id: "trace-openai-responses-samefmt-stream-123".to_string(),
report_kind: "openai_responses_sync_finalize".to_string(),
report_context: Some(json!({
"client_api_format": "openai:cli",
"provider_api_format": "openai:cli",
"client_api_format": "openai:responses",
"provider_api_format": "openai:responses",
"model": "gpt-5.4",
"mapped_model": "gpt-5.4",
"needs_conversion": false,
@@ -1415,7 +1417,7 @@ fn local_finalize_preserves_provider_stream_body_for_same_format_stream_aggregat
};
let outcome = maybe_build_local_core_sync_finalize_response(
"trace-openai-cli-samefmt-stream-123",
"trace-openai-responses-samefmt-stream-123",
&test_decision(),
&payload,
)
@@ -1556,13 +1558,13 @@ fn local_finalize_handles_openai_chat_cross_format_sync_response_from_gemini() {
}
#[test]
fn local_finalize_handles_openai_chat_cross_format_sync_response_from_openai_cli() {
fn local_finalize_handles_openai_chat_cross_format_sync_response_from_openai_responses() {
let payload = GatewaySyncReportRequest {
trace_id: "trace-openai-chat-xfmt-openai-cli-sync-123".to_string(),
trace_id: "trace-openai-chat-xfmt-openai-responses-sync-123".to_string(),
report_kind: "openai_chat_sync_finalize".to_string(),
report_context: Some(json!({
"client_api_format": "openai:chat",
"provider_api_format": "openai:cli",
"provider_api_format": "openai:responses",
"model": "gpt-5.4",
"mapped_model": "gpt-5.4",
"needs_conversion": true,
@@ -1607,7 +1609,7 @@ fn local_finalize_handles_openai_chat_cross_format_sync_response_from_openai_cli
};
let outcome = maybe_build_local_core_sync_finalize_response(
"trace-openai-chat-xfmt-openai-cli-sync-123",
"trace-openai-chat-xfmt-openai-responses-sync-123",
&test_decision(),
&payload,
)

View File

@@ -31,11 +31,12 @@ pub(crate) use self::planner::{
build_local_image_sync_plan_and_reports_for_kind,
build_local_openai_chat_stream_plan_and_reports_for_kind,
build_local_openai_chat_sync_plan_and_reports_for_kind,
build_local_openai_cli_stream_plan_and_reports_for_kind,
build_local_openai_cli_sync_plan_and_reports_for_kind,
build_local_openai_responses_stream_plan_and_reports_for_kind,
build_local_openai_responses_sync_plan_and_reports_for_kind,
build_local_same_format_stream_plan_and_reports, build_local_same_format_sync_plan_and_reports,
build_local_video_sync_plan_and_reports_for_kind, build_openai_cli_stream_plan_from_decision,
build_openai_cli_sync_plan_from_decision, build_passthrough_sync_plan_from_decision,
build_local_video_sync_plan_and_reports_for_kind,
build_openai_responses_stream_plan_from_decision,
build_openai_responses_sync_plan_from_decision, build_passthrough_sync_plan_from_decision,
build_standard_family_stream_plan_and_reports, build_standard_family_sync_plan_and_reports,
build_standard_stream_plan_from_decision, build_standard_sync_plan_from_decision,
extract_pool_sticky_session_token, maybe_build_stream_decision_payload,

View File

@@ -88,10 +88,10 @@ async fn rank_local_execution_candidates(
ordering_config,
)
.await;
let is_same_format = candidate
.endpoint_api_format
.trim()
.eq_ignore_ascii_case(normalized_client_api_format.as_str());
let is_same_format = api_format_matches(
&candidate.endpoint_api_format,
normalized_client_api_format.as_str(),
);
let demote_cross_format = !is_same_format && !ordering.keep_priority_on_conversion;
let format_preference = candidate_api_format_preference(
normalized_client_api_format.as_str(),
@@ -142,9 +142,8 @@ pub(crate) async fn rank_eligible_local_execution_candidates(
ordering_config,
)
.await;
let is_same_format = eligible
.provider_api_format
.eq_ignore_ascii_case(normalized_client_api_format);
let is_same_format =
api_format_matches(&eligible.provider_api_format, normalized_client_api_format);
let demote_cross_format = !is_same_format && !ordering.keep_priority_on_conversion;
let format_preference = candidate_api_format_preference(
normalized_client_api_format,
@@ -176,6 +175,18 @@ pub(crate) async fn rank_eligible_local_execution_candidates(
candidates
}
fn normalize_api_format_alias(value: &str) -> String {
match value.trim().to_ascii_lowercase().as_str() {
"openai:cli" => "openai:responses".to_string(),
"openai:compact" => "openai:responses:compact".to_string(),
other => other.to_string(),
}
}
fn api_format_matches(left: &str, right: &str) -> bool {
normalize_api_format_alias(left) == normalize_api_format_alias(right)
}
pub(crate) fn remember_scheduler_affinity_for_candidate(
state: PlannerAppState<'_>,
auth_snapshot: Option<&GatewayAuthApiKeySnapshot>,
@@ -991,14 +1002,18 @@ mod tests {
vec![sample_provider_with_options("provider-shared", false, 0)],
vec![
sample_endpoint_for_provider("provider-shared", "aaa-claude-chat", "claude:chat"),
sample_endpoint_for_provider("provider-shared", "zzz-openai-cli", "openai:cli"),
sample_endpoint_for_provider(
"provider-shared",
"zzz-openai-responses",
"openai:responses",
),
],
vec![sample_key_for_provider_with_options(
"provider-shared",
"key-shared",
"",
true,
Some(json!(["claude:chat", "openai:cli"])),
Some(json!(["claude:chat", "openai:responses"])),
None,
)],
);
@@ -1023,9 +1038,9 @@ mod tests {
),
sample_priority_candidate(
"provider-shared",
"zzz-openai-cli",
"zzz-openai-responses",
"key-shared",
"openai:cli",
"openai:responses",
Some(0),
0,
),
@@ -1035,7 +1050,7 @@ mod tests {
)
.await;
assert_eq!(ranked[0].endpoint_id, "zzz-openai-cli");
assert_eq!(ranked[0].endpoint_id, "zzz-openai-responses");
assert_eq!(ranked[1].endpoint_id, "aaa-claude-chat");
}

View File

@@ -223,6 +223,7 @@ fn current_local_execution_candidate_common_skip_reason_with_transport(
.endpoint_api_format
.trim()
.eq_ignore_ascii_case(endpoint_api_format)
&& !api_format_matches(&candidate.endpoint_api_format, endpoint_api_format)
{
return Some("endpoint_api_format_changed");
}
@@ -242,7 +243,7 @@ fn candidate_is_ineligible_due_to_disabled_format_conversion(
normalized_client_api_format: &str,
) -> bool {
let endpoint_api_format = transport.endpoint.api_format.trim();
if endpoint_api_format.eq_ignore_ascii_case(normalized_client_api_format) {
if api_format_matches(endpoint_api_format, normalized_client_api_format) {
return false;
}
@@ -277,7 +278,7 @@ fn current_local_execution_candidate_skip_reason_with_transport(
}
let endpoint_api_format = transport.endpoint.api_format.trim();
if endpoint_api_format.eq_ignore_ascii_case(normalized_client_api_format) {
if api_format_matches(endpoint_api_format, normalized_client_api_format) {
return None;
}
@@ -316,10 +317,22 @@ fn transport_key_supports_api_format(
None => true,
Some(formats) => formats
.iter()
.any(|value| value.trim().eq_ignore_ascii_case(endpoint_api_format)),
.any(|value| api_format_matches(value, endpoint_api_format)),
}
}
fn normalize_api_format_alias(value: &str) -> String {
match value.trim().to_ascii_lowercase().as_str() {
"openai:cli" => "openai:responses".to_string(),
"openai:compact" => "openai:responses:compact".to_string(),
other => other.to_string(),
}
}
fn api_format_matches(left: &str, right: &str) -> bool {
normalize_api_format_alias(left) == normalize_api_format_alias(right)
}
fn transport_key_allows_candidate_model(
transport: &GatewayProviderTransportSnapshot,
requested_model: &str,

View File

@@ -391,7 +391,7 @@ mod tests {
provider_type: "codex".to_string(),
provider_priority: 22,
endpoint_id: "endpoint-1".to_string(),
endpoint_api_format: "openai:cli".to_string(),
endpoint_api_format: "openai:responses".to_string(),
key_id: "key-1".to_string(),
key_name: "codex".to_string(),
key_auth_type: "oauth".to_string(),
@@ -426,7 +426,7 @@ mod tests {
endpoint: GatewayProviderTransportEndpoint {
id: "endpoint-1".to_string(),
provider_id: "provider-1".to_string(),
api_format: "openai:cli".to_string(),
api_format: "openai:responses".to_string(),
api_family: None,
endpoint_kind: None,
is_active: true,
@@ -523,12 +523,12 @@ mod tests {
let metadata = build_local_execution_candidate_contract_metadata_for_candidate(
&sample_candidate(),
Some(&sample_transport()),
"openai:cli",
"openai:responses",
"claude:cli",
serde_json::Map::new(),
ExecutionStrategy::LocalCrossFormat,
ConversionMode::Bidirectional,
"openai:cli",
"openai:responses",
);
assert_eq!(metadata["transport_diagnostics"]["provider_type"], "codex");
@@ -555,8 +555,8 @@ mod tests {
let metadata = build_local_execution_candidate_metadata_for_candidate(
&sample_candidate(),
None,
"openai:cli",
"openai:cli",
"openai:responses",
"openai:responses",
serde_json::Map::new(),
);

View File

@@ -11,6 +11,8 @@ pub(crate) use crate::ai_pipeline::contracts::{
GEMINI_VIDEO_CREATE_SYNC_PLAN_KIND, OPENAI_CHAT_STREAM_PLAN_KIND, OPENAI_CHAT_SYNC_PLAN_KIND,
OPENAI_CLI_STREAM_PLAN_KIND, OPENAI_CLI_SYNC_PLAN_KIND, OPENAI_COMPACT_STREAM_PLAN_KIND,
OPENAI_COMPACT_SYNC_PLAN_KIND, OPENAI_IMAGE_STREAM_PLAN_KIND, OPENAI_IMAGE_SYNC_PLAN_KIND,
OPENAI_RESPONSES_COMPACT_STREAM_PLAN_KIND, OPENAI_RESPONSES_COMPACT_SYNC_PLAN_KIND,
OPENAI_RESPONSES_STREAM_PLAN_KIND, OPENAI_RESPONSES_SYNC_PLAN_KIND,
OPENAI_VIDEO_CANCEL_SYNC_PLAN_KIND, OPENAI_VIDEO_CONTENT_PLAN_KIND,
OPENAI_VIDEO_CREATE_SYNC_PLAN_KIND, OPENAI_VIDEO_DELETE_SYNC_PLAN_KIND,
OPENAI_VIDEO_REMIX_SYNC_PLAN_KIND,
@@ -131,12 +133,20 @@ mod tests {
use serde_json::json;
#[test]
fn forces_streaming_for_codex_openai_cli() {
fn forces_streaming_for_codex_openai_responses_and_alias() {
assert!(force_upstream_streaming_for_provider(
"codex",
"openai:responses"
));
assert!(force_upstream_streaming_for_provider("codex", "openai:cli"));
}
#[test]
fn does_not_force_streaming_for_compact_or_other_provider_types() {
assert!(!force_upstream_streaming_for_provider(
"codex",
"openai:responses:compact"
));
assert!(!force_upstream_streaming_for_provider(
"codex",
"openai:compact"

View File

@@ -8,6 +8,8 @@ use crate::ai_pipeline::planner::common::{
GEMINI_VIDEO_CREATE_SYNC_PLAN_KIND, OPENAI_CHAT_STREAM_PLAN_KIND, OPENAI_CHAT_SYNC_PLAN_KIND,
OPENAI_CLI_STREAM_PLAN_KIND, OPENAI_CLI_SYNC_PLAN_KIND, OPENAI_COMPACT_STREAM_PLAN_KIND,
OPENAI_COMPACT_SYNC_PLAN_KIND, OPENAI_IMAGE_STREAM_PLAN_KIND, OPENAI_IMAGE_SYNC_PLAN_KIND,
OPENAI_RESPONSES_COMPACT_STREAM_PLAN_KIND, OPENAI_RESPONSES_COMPACT_SYNC_PLAN_KIND,
OPENAI_RESPONSES_STREAM_PLAN_KIND, OPENAI_RESPONSES_SYNC_PLAN_KIND,
OPENAI_VIDEO_CANCEL_SYNC_PLAN_KIND, OPENAI_VIDEO_CONTENT_PLAN_KIND,
OPENAI_VIDEO_CREATE_SYNC_PLAN_KIND, OPENAI_VIDEO_DELETE_SYNC_PLAN_KIND,
OPENAI_VIDEO_REMIX_SYNC_PLAN_KIND,
@@ -15,10 +17,10 @@ use crate::ai_pipeline::planner::common::{
use crate::ai_pipeline::planner::plan_builders::{
build_gemini_stream_plan_from_decision, build_gemini_sync_plan_from_decision,
build_openai_chat_stream_plan_from_decision, build_openai_chat_sync_plan_from_decision,
build_openai_cli_stream_plan_from_decision, build_openai_cli_sync_plan_from_decision,
build_passthrough_stream_plan_from_decision, build_passthrough_sync_plan_from_decision,
build_standard_stream_plan_from_decision, build_standard_sync_plan_from_decision,
LocalStreamPlanAndReport, LocalSyncPlanAndReport,
build_openai_responses_stream_plan_from_decision,
build_openai_responses_sync_plan_from_decision, build_passthrough_stream_plan_from_decision,
build_passthrough_sync_plan_from_decision, build_standard_stream_plan_from_decision,
build_standard_sync_plan_from_decision, LocalStreamPlanAndReport, LocalSyncPlanAndReport,
};
use crate::ai_pipeline::planner::route::{
resolve_execution_runtime_stream_plan_kind as resolve_stream_plan_kind,
@@ -96,12 +98,12 @@ fn build_sync_plan_payload_from_decision(
OPENAI_CHAT_SYNC_PLAN_KIND => {
build_openai_chat_sync_plan_from_decision(parts, body_json, payload)?
}
OPENAI_CLI_SYNC_PLAN_KIND => {
build_openai_cli_sync_plan_from_decision(parts, body_json, payload, false)?
OPENAI_RESPONSES_SYNC_PLAN_KIND | OPENAI_CLI_SYNC_PLAN_KIND => {
build_openai_responses_sync_plan_from_decision(parts, body_json, payload, false)?
}
OPENAI_IMAGE_SYNC_PLAN_KIND => build_passthrough_sync_plan_from_decision(parts, payload)?,
OPENAI_COMPACT_SYNC_PLAN_KIND => {
build_openai_cli_sync_plan_from_decision(parts, body_json, payload, true)?
OPENAI_RESPONSES_COMPACT_SYNC_PLAN_KIND | OPENAI_COMPACT_SYNC_PLAN_KIND => {
build_openai_responses_sync_plan_from_decision(parts, body_json, payload, true)?
}
CLAUDE_CHAT_SYNC_PLAN_KIND | CLAUDE_CLI_SYNC_PLAN_KIND => {
build_standard_sync_plan_from_decision(parts, body_json, payload)?
@@ -137,14 +139,14 @@ fn build_stream_plan_payload_from_decision(
OPENAI_CHAT_STREAM_PLAN_KIND => {
build_openai_chat_stream_plan_from_decision(parts, body_json, payload)?
}
OPENAI_CLI_STREAM_PLAN_KIND => {
build_openai_cli_stream_plan_from_decision(parts, body_json, payload, false)?
OPENAI_RESPONSES_STREAM_PLAN_KIND | OPENAI_CLI_STREAM_PLAN_KIND => {
build_openai_responses_stream_plan_from_decision(parts, body_json, payload, false)?
}
OPENAI_IMAGE_STREAM_PLAN_KIND => {
build_standard_stream_plan_from_decision(parts, body_json, payload, false)?
}
OPENAI_COMPACT_STREAM_PLAN_KIND => {
build_openai_cli_stream_plan_from_decision(parts, body_json, payload, true)?
OPENAI_RESPONSES_COMPACT_STREAM_PLAN_KIND | OPENAI_COMPACT_STREAM_PLAN_KIND => {
build_openai_responses_stream_plan_from_decision(parts, body_json, payload, true)?
}
CLAUDE_CHAT_STREAM_PLAN_KIND | CLAUDE_CLI_STREAM_PLAN_KIND => {
build_standard_stream_plan_from_decision(parts, body_json, payload, true)?

View File

@@ -19,8 +19,8 @@ pub(crate) use super::specialized::{
};
pub(crate) use super::standard::{
maybe_build_stream_local_decision_payload,
maybe_build_stream_local_openai_cli_decision_payload,
maybe_build_stream_local_openai_responses_decision_payload,
maybe_build_stream_local_standard_decision_payload, maybe_build_sync_local_decision_payload,
maybe_build_sync_local_openai_cli_decision_payload,
maybe_build_sync_local_openai_responses_decision_payload,
maybe_build_sync_local_standard_decision_payload,
};

View File

@@ -58,7 +58,7 @@ pub(crate) async fn maybe_build_stream_decision_payload(
return Ok(Some(payload));
}
if let Some(payload) = super::maybe_build_stream_local_openai_cli_decision_payload(
if let Some(payload) = super::maybe_build_stream_local_openai_responses_decision_payload(
state, parts, trace_id, decision, body_json, plan_kind,
)
.await?

View File

@@ -67,7 +67,7 @@ pub(crate) async fn maybe_build_sync_decision_payload(
return Ok(Some(payload));
}
if let Some(payload) = super::maybe_build_sync_local_openai_cli_decision_payload(
if let Some(payload) = super::maybe_build_sync_local_openai_responses_decision_payload(
state, parts, trace_id, decision, body_json, plan_kind,
)
.await?

View File

@@ -11,7 +11,7 @@ pub(crate) enum LocalCandidatePersistencePolicyKind {
StandardDecision,
SameFormatProviderDecision,
OpenAiChatDecision,
OpenAiCliDecision,
OpenAiResponsesDecision,
ImageDecision,
GeminiFilesDecision,
VideoDecision,
@@ -45,9 +45,9 @@ pub(crate) fn build_local_candidate_persistence_policy<'a>(
"gateway local openai chat decision failed to persist skipped candidate",
true,
),
LocalCandidatePersistencePolicyKind::OpenAiCliDecision => (
"gateway local openai cli decision request candidate upsert failed",
"gateway local openai cli decision failed to persist skipped candidate",
LocalCandidatePersistencePolicyKind::OpenAiResponsesDecision => (
"gateway local openai responses decision request candidate upsert failed",
"gateway local openai responses decision failed to persist skipped candidate",
true,
),
LocalCandidatePersistencePolicyKind::ImageDecision => (

View File

@@ -36,9 +36,10 @@ pub(crate) use self::passthrough::{
};
pub(crate) use self::plan_builders::{
build_gemini_stream_plan_from_decision, build_gemini_sync_plan_from_decision,
build_openai_cli_stream_plan_from_decision, build_openai_cli_sync_plan_from_decision,
build_passthrough_sync_plan_from_decision, build_standard_stream_plan_from_decision,
build_standard_sync_plan_from_decision, LocalStreamPlanAndReport, LocalSyncPlanAndReport,
build_openai_responses_stream_plan_from_decision,
build_openai_responses_sync_plan_from_decision, build_passthrough_sync_plan_from_decision,
build_standard_stream_plan_from_decision, build_standard_sync_plan_from_decision,
LocalStreamPlanAndReport, LocalSyncPlanAndReport,
};
pub(crate) use self::route::is_matching_stream_request as planner_is_matching_stream_request;
pub(crate) use self::specialized::{
@@ -51,8 +52,8 @@ pub(crate) use self::specialized::{
pub(crate) use self::standard::{
build_local_openai_chat_stream_plan_and_reports_for_kind,
build_local_openai_chat_sync_plan_and_reports_for_kind,
build_local_openai_cli_stream_plan_and_reports_for_kind,
build_local_openai_cli_sync_plan_and_reports_for_kind,
build_local_openai_responses_stream_plan_and_reports_for_kind,
build_local_openai_responses_sync_plan_and_reports_for_kind,
build_local_stream_plan_and_reports as build_standard_family_stream_plan_and_reports,
build_local_sync_plan_and_reports as build_standard_family_sync_plan_and_reports,
set_local_openai_chat_execution_exhausted_diagnostic,

View File

@@ -38,8 +38,8 @@ pub(crate) fn resolve_same_format_provider_transport_unsupported_reason_for_trac
) -> Option<&'static str> {
let provider_api_format = match provider_api_format.trim().to_ascii_lowercase().as_str() {
"openai:chat" => "openai:chat",
"openai:cli" => "openai:cli",
"openai:compact" => "openai:compact",
"openai:responses" | "openai:cli" => "openai:responses",
"openai:responses:compact" | "openai:compact" => "openai:responses:compact",
"claude:chat" => "claude:chat",
"claude:cli" => "claude:cli",
"gemini:chat" => "gemini:chat",

View File

@@ -19,7 +19,8 @@ pub(crate) use gemini_builders::{
};
pub(crate) use openai_builders::{
build_openai_chat_stream_plan_from_decision, build_openai_chat_sync_plan_from_decision,
build_openai_cli_stream_plan_from_decision, build_openai_cli_sync_plan_from_decision,
build_openai_responses_stream_plan_from_decision,
build_openai_responses_sync_plan_from_decision,
};
pub(crate) use passthrough_builders::{
build_passthrough_stream_plan_from_decision, build_passthrough_sync_plan_from_decision,

View File

@@ -208,7 +208,7 @@ pub(crate) fn provider_stream_event_api_format_for_provider_type(
provider_type: &str,
) -> Option<&'static str> {
match provider_type.trim().to_ascii_lowercase().as_str() {
"codex" => Some("openai:cli"),
"codex" => Some("openai:responses"),
_ => None,
}
}
@@ -230,14 +230,14 @@ mod tests {
use super::provider_stream_event_api_format_for_provider_type;
#[test]
fn codex_provider_uses_openai_cli_stream_event_format() {
fn codex_provider_uses_openai_responses_stream_event_format() {
assert_eq!(
provider_stream_event_api_format_for_provider_type("codex"),
Some("openai:cli")
Some("openai:responses")
);
assert_eq!(
provider_stream_event_api_format_for_provider_type("CODEX"),
Some("openai:cli")
Some("openai:responses")
);
}

View File

@@ -5,8 +5,8 @@ use crate::ai_pipeline::planner::plan_builders::{
LocalStreamPlanAndReport, LocalSyncPlanAndReport,
};
use crate::ai_pipeline::{
GatewayControlSyncDecisionResponse, LocalGeminiFilesSpec, LocalOpenAiCliSpec,
LocalOpenAiImageSpec, LocalSameFormatProviderFamily, LocalSameFormatProviderSpec,
GatewayControlSyncDecisionResponse, LocalGeminiFilesSpec, LocalOpenAiImageSpec,
LocalOpenAiResponsesSpec, LocalSameFormatProviderFamily, LocalSameFormatProviderSpec,
LocalStandardSourceFamily, LocalStandardSpec, LocalVideoCreateFamily, LocalVideoCreateSpec,
};
use crate::GatewayError;
@@ -53,8 +53,8 @@ pub(crate) fn local_same_format_provider_spec_metadata(
}
}
pub(crate) fn local_openai_cli_spec_metadata(
spec: LocalOpenAiCliSpec,
pub(crate) fn local_openai_responses_spec_metadata(
spec: LocalOpenAiResponsesSpec,
) -> LocalExecutionSurfaceSpecMetadata {
LocalExecutionSurfaceSpecMetadata {
api_format: spec.api_format,

View File

@@ -11,12 +11,12 @@ use crate::ai_pipeline::planner::spec_metadata::local_openai_image_spec_metadata
use crate::ai_pipeline::transport::auth::{
build_passthrough_headers_with_auth, resolve_local_openai_bearer_auth,
};
use crate::ai_pipeline::transport::url::build_openai_cli_url;
use crate::ai_pipeline::transport::url::build_openai_responses_url;
use crate::ai_pipeline::transport::{
apply_local_header_rules, local_standard_transport_unsupported_reason_with_network,
};
use crate::ai_pipeline::{
apply_codex_openai_cli_special_body_edits, apply_codex_openai_cli_special_headers,
apply_codex_openai_responses_special_body_edits, apply_codex_openai_responses_special_headers,
CandidateFailureDiagnostic, GatewayProviderTransportSnapshot, PlannerAppState,
CODEX_OPENAI_IMAGE_DEFAULT_MODEL, CODEX_OPENAI_IMAGE_DEFAULT_VARIATION_MODEL,
};
@@ -209,9 +209,10 @@ pub(super) async fn resolve_local_openai_image_candidate_payload_parts(
return None;
};
let upstream_url = build_openai_cli_url(&transport.endpoint.base_url, parts.uri.query(), false);
let upstream_url =
build_openai_responses_url(&transport.endpoint.base_url, parts.uri.query(), false);
let mut provider_request_body = build_provider_request_body(&normalized_request);
apply_codex_openai_cli_special_body_edits(
apply_codex_openai_responses_special_body_edits(
&mut provider_request_body,
transport.provider.provider_type.as_str(),
spec_metadata.api_format,
@@ -251,7 +252,7 @@ pub(super) async fn resolve_local_openai_image_candidate_payload_parts(
.await;
return None;
}
apply_codex_openai_cli_special_headers(
apply_codex_openai_responses_special_headers(
&mut provider_request_headers,
&provider_request_body,
&parts.headers,
@@ -1031,7 +1032,7 @@ mod tests {
assert_eq!(request.images.len(), 1);
let mut provider_request_body = build_provider_request_body(&request);
super::apply_codex_openai_cli_special_body_edits(
super::apply_codex_openai_responses_special_body_edits(
&mut provider_request_body,
"codex",
"openai:image",
@@ -1172,7 +1173,7 @@ mod tests {
assert!(provider_request_body.get("model").is_none());
assert!(provider_request_body.get("tool_choice").is_none());
assert!(provider_request_body.get("stream").is_none());
super::apply_codex_openai_cli_special_body_edits(
super::apply_codex_openai_responses_special_body_edits(
&mut provider_request_body,
"codex",
"openai:image",

View File

@@ -3,5 +3,5 @@
mod tests;
pub(crate) use crate::ai_pipeline::{
apply_codex_openai_cli_special_body_edits, apply_codex_openai_cli_special_headers,
apply_codex_openai_responses_special_body_edits, apply_codex_openai_responses_special_headers,
};

View File

@@ -1,6 +1,8 @@
use std::collections::BTreeMap;
use super::{apply_codex_openai_cli_special_body_edits, apply_codex_openai_cli_special_headers};
use super::{
apply_codex_openai_responses_special_body_edits, apply_codex_openai_responses_special_headers,
};
use http::{HeaderMap, HeaderValue};
use serde_json::json;
@@ -15,7 +17,13 @@ fn applies_codex_defaults_when_body_rules_do_not_handle_fields() {
"store": true
});
apply_codex_openai_cli_special_body_edits(&mut body, "codex", "openai:cli", None, None);
apply_codex_openai_responses_special_body_edits(
&mut body,
"codex",
"openai:responses",
None,
None,
);
assert!(body.get("max_output_tokens").is_none());
assert!(body.get("temperature").is_none());
@@ -42,10 +50,10 @@ fn strips_store_for_compact_even_when_body_rules_handle_it() {
"top_p": 0.5
});
apply_codex_openai_cli_special_body_edits(
apply_codex_openai_responses_special_body_edits(
&mut body,
"codex",
"openai:compact",
"openai:responses:compact",
Some(&body_rules),
None,
);
@@ -64,10 +72,10 @@ fn injects_stable_prompt_cache_key_for_codex_requests() {
"input": "hello",
});
apply_codex_openai_cli_special_body_edits(
apply_codex_openai_responses_special_body_edits(
&mut body,
"codex",
"openai:cli",
"openai:responses",
None,
Some("key-123"),
);
@@ -86,10 +94,10 @@ fn keeps_existing_prompt_cache_key_for_codex_requests() {
"prompt_cache_key": "existing-key",
});
apply_codex_openai_cli_special_body_edits(
apply_codex_openai_responses_special_body_edits(
&mut body,
"codex",
"openai:cli",
"openai:responses",
None,
Some("key-123"),
);
@@ -105,12 +113,12 @@ fn injects_chatgpt_account_id_and_session_headers_for_codex_requests() {
"prompt_cache_key": "172c39e6-c0a0-5a70-8b63-e0f8e0d185a3",
});
apply_codex_openai_cli_special_headers(
apply_codex_openai_responses_special_headers(
&mut headers,
&body,
&HeaderMap::new(),
"codex",
"openai:cli",
"openai:responses",
Some("trace-codex-123"),
Some(r#"{"account_id":"acc-123"}"#),
);
@@ -175,12 +183,12 @@ fn respects_existing_codex_request_and_session_headers() {
HeaderValue::from_static("user-specified-originator"),
);
apply_codex_openai_cli_special_headers(
apply_codex_openai_responses_special_headers(
&mut headers,
&body,
&original_headers,
"codex",
"openai:cli",
"openai:responses",
Some("trace-codex-123"),
Some(r#"{"account_id":"acc-123"}"#),
);
@@ -203,12 +211,12 @@ fn skips_conversation_id_for_compact_codex_requests() {
"prompt_cache_key": "172c39e6-c0a0-5a70-8b63-e0f8e0d185a3",
});
apply_codex_openai_cli_special_headers(
apply_codex_openai_responses_special_headers(
&mut headers,
&body,
&HeaderMap::new(),
"codex",
"openai:compact",
"openai:responses:compact",
Some("trace-codex-compact-123"),
Some(r#"{"account_id":"acc-123"}"#),
);

View File

@@ -9,7 +9,7 @@ use crate::ai_pipeline::planner::candidate_preparation::{
use crate::ai_pipeline::planner::common::force_upstream_streaming_for_provider;
use crate::ai_pipeline::planner::spec_metadata::local_standard_spec_metadata;
use crate::ai_pipeline::planner::standard::{
apply_codex_openai_cli_special_headers, request_body_build_failure_extra_data,
apply_codex_openai_responses_special_headers, request_body_build_failure_extra_data,
};
use crate::ai_pipeline::transport::apply_local_header_rules;
use crate::ai_pipeline::transport::auth::{
@@ -314,7 +314,7 @@ pub(crate) async fn resolve_local_standard_candidate_payload_parts(
.await;
return None;
}
apply_codex_openai_cli_special_headers(
apply_codex_openai_responses_special_headers(
&mut provider_request_headers,
&provider_request_body,
&parts.headers,

View File

@@ -14,25 +14,27 @@ mod normalize;
mod openai;
mod request_body_diagnostics;
pub(crate) use self::codex::apply_codex_openai_cli_special_headers;
pub(crate) use self::codex::apply_codex_openai_responses_special_headers;
pub(crate) use self::family::{
build_local_stream_plan_and_reports, build_local_sync_plan_and_reports,
};
pub(crate) use self::normalize::{
build_cross_format_openai_chat_request_body, build_cross_format_openai_chat_upstream_url,
build_cross_format_openai_cli_request_body, build_cross_format_openai_cli_upstream_url,
build_local_openai_chat_request_body, build_local_openai_chat_upstream_url,
build_local_openai_cli_request_body, build_local_openai_cli_upstream_url,
build_cross_format_openai_responses_request_body,
build_cross_format_openai_responses_upstream_url, build_local_openai_chat_request_body,
build_local_openai_chat_upstream_url, build_local_openai_responses_request_body,
build_local_openai_responses_upstream_url,
};
pub(crate) use self::openai::{
build_local_openai_chat_stream_plan_and_reports_for_kind,
build_local_openai_chat_sync_plan_and_reports_for_kind,
build_local_openai_cli_stream_plan_and_reports_for_kind,
build_local_openai_cli_sync_plan_and_reports_for_kind, copy_request_number_field,
build_local_openai_responses_stream_plan_and_reports_for_kind,
build_local_openai_responses_sync_plan_and_reports_for_kind, copy_request_number_field,
copy_request_number_field_as, map_openai_reasoning_effort_to_claude_output,
map_openai_reasoning_effort_to_gemini_budget, maybe_build_stream_local_decision_payload,
maybe_build_stream_local_openai_cli_decision_payload, maybe_build_sync_local_decision_payload,
maybe_build_sync_local_openai_cli_decision_payload, parse_openai_stop_sequences,
maybe_build_stream_local_openai_responses_decision_payload,
maybe_build_sync_local_decision_payload,
maybe_build_sync_local_openai_responses_decision_payload, parse_openai_stop_sequences,
resolve_openai_chat_max_tokens, set_local_openai_chat_execution_exhausted_diagnostic,
value_as_u64,
};
@@ -49,8 +51,8 @@ pub(crate) use crate::ai_pipeline::normalize_standard_request_to_openai_chat_req
pub(crate) use crate::ai_pipeline::{
build_standard_request_body, build_standard_upstream_url,
convert_openai_chat_request_to_claude_request, convert_openai_chat_request_to_gemini_request,
convert_openai_chat_request_to_openai_cli_request, extract_openai_text_content,
normalize_openai_cli_request_to_openai_chat_request, parse_openai_tool_result_content,
convert_openai_chat_request_to_openai_responses_request, extract_openai_text_content,
normalize_openai_responses_request_to_openai_chat_request, parse_openai_tool_result_content,
};
pub(crate) async fn maybe_build_sync_local_standard_decision_payload(
@@ -207,13 +209,13 @@ mod tests {
}
#[test]
fn builds_openai_cli_request_from_claude_cli_source_with_forced_stream() {
fn builds_openai_responses_request_from_claude_cli_source_with_forced_stream() {
let request = json!({
"model": "claude-sonnet-4-5",
"messages": [
{
"role": "user",
"content": [{"type": "text", "text": "Need OpenAI CLI output"}]
"content": [{"type": "text", "text": "Need OpenAI Responses output"}]
}
],
"max_tokens": 64
@@ -224,32 +226,32 @@ mod tests {
"claude:cli",
"gpt-5",
"openai",
"openai:cli",
"openai:responses",
"/v1/messages",
true,
None,
None,
)
.expect("claude cli should convert to openai cli");
.expect("claude cli should convert to openai responses");
assert_eq!(converted["model"], "gpt-5");
assert_eq!(converted["input"][0]["role"], "user");
assert_eq!(converted["input"][0]["content"][0]["type"], "input_text");
assert_eq!(
converted["input"][0]["content"][0]["text"],
"Need OpenAI CLI output"
"Need OpenAI Responses output"
);
assert_eq!(converted["stream"], true);
}
#[test]
fn strips_metadata_for_codex_openai_cli_requests() {
fn strips_metadata_for_codex_openai_responses_requests() {
let request = json!({
"model": "claude-sonnet-4-5",
"metadata": {"trace_id": "abc"},
"messages": [{
"role": "user",
"content": [{"type": "text", "text": "Need OpenAI CLI output"}]
"content": [{"type": "text", "text": "Need OpenAI Responses output"}]
}],
"max_tokens": 64
});
@@ -259,7 +261,7 @@ mod tests {
"claude:cli",
"gpt-5",
"codex",
"openai:cli",
"openai:responses",
"/v1/messages",
false,
None,
@@ -273,7 +275,7 @@ mod tests {
}
#[test]
fn strips_store_for_openai_compact_requests() {
fn strips_store_for_openai_responses_compact_requests() {
let request = json!({
"model": "gpt-5",
"messages": [{
@@ -288,13 +290,13 @@ mod tests {
"openai:chat",
"gpt-5",
"openai",
"openai:compact",
"openai:responses:compact",
"/v1/chat/completions",
false,
None,
None,
)
.expect("openai chat should convert to openai compact");
.expect("openai chat should convert to openai responses compact");
assert_eq!(converted["model"], "gpt-5");
assert!(converted.get("store").is_none());

View File

@@ -1,7 +1,7 @@
#[path = "normalize/chat.rs"]
mod chat;
#[path = "normalize/cli.rs"]
mod cli;
#[path = "normalize/responses.rs"]
mod responses;
#[cfg(test)]
#[path = "normalize/tests.rs"]
mod tests;
@@ -10,7 +10,8 @@ pub(crate) use self::chat::{
build_cross_format_openai_chat_request_body, build_cross_format_openai_chat_upstream_url,
build_local_openai_chat_request_body, build_local_openai_chat_upstream_url,
};
pub(crate) use self::cli::{
build_cross_format_openai_cli_request_body, build_cross_format_openai_cli_upstream_url,
build_local_openai_cli_request_body, build_local_openai_cli_upstream_url,
pub(crate) use self::responses::{
build_cross_format_openai_responses_request_body,
build_cross_format_openai_responses_upstream_url, build_local_openai_responses_request_body,
build_local_openai_responses_upstream_url,
};

View File

@@ -3,11 +3,12 @@ use serde_json::Value;
use crate::ai_pipeline::conversion::{request_conversion_kind, RequestConversionKind};
use crate::ai_pipeline::transport::apply_local_body_rules;
use crate::ai_pipeline::transport::url::{
build_claude_messages_url, build_openai_chat_url, build_openai_cli_url,
build_claude_messages_url, build_openai_chat_url, build_openai_responses_url,
build_passthrough_path_url,
};
use crate::ai_pipeline::{
apply_codex_openai_cli_special_body_edits, apply_openai_compact_special_body_edits,
apply_codex_openai_responses_special_body_edits,
apply_openai_responses_compact_special_body_edits,
build_cross_format_openai_chat_request_body as pipeline_build_cross_format_openai_chat_request_body,
build_local_openai_chat_request_body as pipeline_build_local_openai_chat_request_body,
GatewayProviderTransportSnapshot,
@@ -67,14 +68,17 @@ pub(crate) fn build_cross_format_openai_chat_request_body(
if !apply_local_body_rules(&mut provider_request_body, body_rules, Some(body_json)) {
return None;
}
apply_codex_openai_cli_special_body_edits(
apply_codex_openai_responses_special_body_edits(
&mut provider_request_body,
provider_type,
provider_api_format,
body_rules,
user_api_key_id,
);
apply_openai_compact_special_body_edits(&mut provider_request_body, provider_api_format);
apply_openai_responses_compact_special_body_edits(
&mut provider_request_body,
provider_api_format,
);
Some(provider_request_body)
}
@@ -112,7 +116,7 @@ pub(crate) fn build_cross_format_openai_chat_upstream_url(
None,
)
}
RequestConversionKind::ToOpenAIFamilyCli => Some(build_openai_cli_url(
RequestConversionKind::ToOpenAiResponses => Some(build_openai_responses_url(
&transport.endpoint.base_url,
parts.uri.query(),
false,

View File

@@ -9,17 +9,18 @@ use crate::ai_pipeline::transport::antigravity::{
};
use crate::ai_pipeline::transport::apply_local_body_rules;
use crate::ai_pipeline::transport::url::{
build_claude_messages_url, build_openai_chat_url, build_openai_cli_url,
build_claude_messages_url, build_openai_chat_url, build_openai_responses_url,
build_passthrough_path_url,
};
use crate::ai_pipeline::{
apply_codex_openai_cli_special_body_edits, apply_openai_compact_special_body_edits,
build_cross_format_openai_cli_request_body as pipeline_build_cross_format_openai_cli_request_body,
build_local_openai_cli_request_body as pipeline_build_local_openai_cli_request_body,
apply_codex_openai_responses_special_body_edits,
apply_openai_responses_compact_special_body_edits,
build_cross_format_openai_responses_request_body as pipeline_build_cross_format_openai_responses_request_body,
build_local_openai_responses_request_body as pipeline_build_local_openai_responses_request_body,
GatewayProviderTransportSnapshot,
};
pub(crate) fn build_local_openai_cli_request_body(
pub(crate) fn build_local_openai_responses_request_body(
body_json: &Value,
mapped_model: &str,
require_streaming: bool,
@@ -28,23 +29,29 @@ pub(crate) fn build_local_openai_cli_request_body(
body_rules: Option<&Value>,
user_api_key_id: Option<&str>,
) -> Option<Value> {
let mut provider_request_body =
pipeline_build_local_openai_cli_request_body(body_json, mapped_model, require_streaming)?;
let mut provider_request_body = pipeline_build_local_openai_responses_request_body(
body_json,
mapped_model,
require_streaming,
)?;
if !apply_local_body_rules(&mut provider_request_body, body_rules, Some(body_json)) {
return None;
}
apply_codex_openai_cli_special_body_edits(
apply_codex_openai_responses_special_body_edits(
&mut provider_request_body,
provider_type,
provider_api_format,
body_rules,
user_api_key_id,
);
apply_openai_compact_special_body_edits(&mut provider_request_body, provider_api_format);
apply_openai_responses_compact_special_body_edits(
&mut provider_request_body,
provider_api_format,
);
Some(provider_request_body)
}
pub(crate) fn build_cross_format_openai_cli_request_body(
pub(crate) fn build_cross_format_openai_responses_request_body(
body_json: &Value,
mapped_model: &str,
client_api_format: &str,
@@ -54,7 +61,7 @@ pub(crate) fn build_cross_format_openai_cli_request_body(
body_rules: Option<&Value>,
user_api_key_id: Option<&str>,
) -> Option<Value> {
let mut provider_request_body = pipeline_build_cross_format_openai_cli_request_body(
let mut provider_request_body = pipeline_build_cross_format_openai_responses_request_body(
body_json,
mapped_model,
client_api_format,
@@ -64,18 +71,21 @@ pub(crate) fn build_cross_format_openai_cli_request_body(
if !apply_local_body_rules(&mut provider_request_body, body_rules, Some(body_json)) {
return None;
}
apply_codex_openai_cli_special_body_edits(
apply_codex_openai_responses_special_body_edits(
&mut provider_request_body,
provider_type,
provider_api_format,
body_rules,
user_api_key_id,
);
apply_openai_compact_special_body_edits(&mut provider_request_body, provider_api_format);
apply_openai_responses_compact_special_body_edits(
&mut provider_request_body,
provider_api_format,
);
Some(provider_request_body)
}
pub(crate) fn build_local_openai_cli_upstream_url(
pub(crate) fn build_local_openai_responses_upstream_url(
parts: &http::request::Parts,
transport: &GatewayProviderTransportSnapshot,
compact: bool,
@@ -91,7 +101,7 @@ pub(crate) fn build_local_openai_cli_upstream_url(
Some(path) => {
build_passthrough_path_url(&transport.endpoint.base_url, path, parts.uri.query(), &[])
}
None => Some(build_openai_cli_url(
None => Some(build_openai_responses_url(
&transport.endpoint.base_url,
parts.uri.query(),
compact,
@@ -99,7 +109,7 @@ pub(crate) fn build_local_openai_cli_upstream_url(
}
}
pub(crate) fn build_cross_format_openai_cli_upstream_url(
pub(crate) fn build_cross_format_openai_responses_upstream_url(
parts: &http::request::Parts,
transport: &GatewayProviderTransportSnapshot,
mapped_model: &str,
@@ -146,7 +156,7 @@ pub(crate) fn build_cross_format_openai_cli_upstream_url(
&transport.endpoint.base_url,
parts.uri.query(),
)),
RequestConversionKind::ToOpenAIFamilyCli => Some(build_openai_cli_url(
RequestConversionKind::ToOpenAiResponses => Some(build_openai_responses_url(
&transport.endpoint.base_url,
parts.uri.query(),
false,

View File

@@ -6,8 +6,8 @@ use http::Request;
use serde_json::{json, Value};
use super::{
build_cross_format_openai_cli_request_body, build_local_openai_cli_request_body,
build_local_openai_cli_upstream_url,
build_cross_format_openai_responses_request_body, build_local_openai_responses_request_body,
build_local_openai_responses_upstream_url,
};
fn object_keys(value: &Value) -> Vec<&str> {
@@ -73,13 +73,13 @@ fn sample_transport(base_url: &str, api_format: &str) -> GatewayProviderTranspor
}
#[test]
fn builds_openai_chat_cross_format_request_body_from_openai_cli_source() {
fn builds_openai_chat_cross_format_request_body_from_openai_responses_source() {
let body_json = json!({
"model": "gpt-5",
"input": "hello",
});
let provider_request_body = build_cross_format_openai_cli_request_body(
let provider_request_body = build_cross_format_openai_responses_request_body(
&body_json,
"gpt-5-upstream",
"openai:cli",
@@ -89,7 +89,7 @@ fn builds_openai_chat_cross_format_request_body_from_openai_cli_source() {
None,
None,
)
.expect("openai cli to openai chat body should build");
.expect("openai responses to openai chat body should build");
assert_eq!(provider_request_body["model"], "gpt-5-upstream");
assert_eq!(provider_request_body["messages"][0]["role"], "user");
@@ -97,7 +97,7 @@ fn builds_openai_chat_cross_format_request_body_from_openai_cli_source() {
}
#[test]
fn local_openai_cli_wrapper_preserves_body_order_after_edits() {
fn local_openai_responses_wrapper_preserves_body_order_after_edits() {
let body_json: Value = serde_json::from_str(
r#"{
"text": {"format": {"type": "text"}},
@@ -113,7 +113,7 @@ fn local_openai_cli_wrapper_preserves_body_order_after_edits() {
)
.expect("request body should parse");
let provider_request_body = build_local_openai_cli_request_body(
let provider_request_body = build_local_openai_responses_request_body(
&body_json,
"gpt-5.4",
true,
@@ -122,7 +122,7 @@ fn local_openai_cli_wrapper_preserves_body_order_after_edits() {
None,
Some("key-123"),
)
.expect("local openai cli body should build");
.expect("local openai responses body should build");
assert_eq!(
object_keys(&provider_request_body),
@@ -150,7 +150,7 @@ fn local_openai_compact_wrapper_strips_store_for_same_format_requests() {
"store": true
});
let provider_request_body = build_local_openai_cli_request_body(
let provider_request_body = build_local_openai_responses_request_body(
&body_json,
"gpt-5.4",
false,
@@ -165,7 +165,7 @@ fn local_openai_compact_wrapper_strips_store_for_same_format_requests() {
}
#[test]
fn local_openai_cli_upstream_url_preserves_codex_base_path() {
fn local_openai_responses_upstream_url_preserves_codex_base_path() {
let request = Request::builder()
.method("POST")
.uri("/v1/responses")
@@ -173,18 +173,18 @@ fn local_openai_cli_upstream_url_preserves_codex_base_path() {
.expect("request should build");
let (parts, _) = request.into_parts();
let upstream_url = build_local_openai_cli_upstream_url(
let upstream_url = build_local_openai_responses_upstream_url(
&parts,
&sample_transport("https://tiger.bookapi.cc/codex", "openai:cli"),
false,
)
.expect("openai cli upstream url should build");
.expect("openai responses upstream url should build");
assert_eq!(upstream_url, "https://tiger.bookapi.cc/codex/responses");
}
#[test]
fn strips_metadata_for_codex_openai_cli_requests() {
fn strips_metadata_for_codex_openai_responses_requests() {
let body_json = json!({
"model": "claude-sonnet-4-5",
"metadata": {"trace_id": "abc"},
@@ -194,7 +194,7 @@ fn strips_metadata_for_codex_openai_cli_requests() {
}],
});
let provider_request_body = build_cross_format_openai_cli_request_body(
let provider_request_body = build_cross_format_openai_responses_request_body(
&body_json,
"gpt-5-upstream",
"claude:cli",
@@ -226,7 +226,7 @@ fn applies_codex_defaults_unless_body_rules_handle_the_field() {
{"action":"set","path":"metadata","value":{"trace_id":"keep-me"}}
]);
let provider_request_body = build_cross_format_openai_cli_request_body(
let provider_request_body = build_cross_format_openai_responses_request_body(
&body_json,
"gpt-5-upstream",
"claude:cli",
@@ -244,7 +244,7 @@ fn applies_codex_defaults_unless_body_rules_handle_the_field() {
}
#[test]
fn injects_codex_prompt_cache_key_for_openai_cli_cross_format_requests() {
fn injects_codex_prompt_cache_key_for_openai_responses_cross_format_requests() {
let body_json = json!({
"model": "claude-sonnet-4-5",
"messages": [{
@@ -253,7 +253,7 @@ fn injects_codex_prompt_cache_key_for_openai_cli_cross_format_requests() {
}],
});
let provider_request_body = build_cross_format_openai_cli_request_body(
let provider_request_body = build_cross_format_openai_responses_request_body(
&body_json,
"gpt-5-upstream",
"claude:cli",

View File

@@ -10,7 +10,7 @@ use crate::ai_pipeline::planner::candidate_preparation::{
};
use crate::ai_pipeline::planner::common::OPENAI_CHAT_STREAM_PLAN_KIND;
use crate::ai_pipeline::planner::standard::{
apply_codex_openai_cli_special_headers, build_cross_format_openai_chat_request_body,
apply_codex_openai_responses_special_headers, build_cross_format_openai_chat_request_body,
build_cross_format_openai_chat_upstream_url, build_local_openai_chat_request_body,
build_local_openai_chat_upstream_url, request_body_build_failure_extra_data,
};
@@ -190,7 +190,7 @@ pub(crate) async fn resolve_local_openai_chat_candidate_payload_parts(
.await;
return None;
}
apply_codex_openai_cli_special_headers(
apply_codex_openai_responses_special_headers(
&mut provider_request_headers,
&provider_request_body,
&parts.headers,
@@ -476,7 +476,7 @@ pub(crate) async fn resolve_local_openai_chat_candidate_payload_parts(
.await;
return None;
}
apply_codex_openai_cli_special_headers(
apply_codex_openai_responses_special_headers(
&mut provider_request_headers,
&provider_request_body,
&parts.headers,

View File

@@ -140,7 +140,11 @@ mod tests {
use super::openai_chat_sync_upstream_is_stream_for_candidate;
#[test]
fn openai_chat_sync_forces_streaming_for_codex_openai_cli_candidates() {
fn openai_chat_sync_forces_streaming_for_codex_openai_responses_candidates() {
assert!(openai_chat_sync_upstream_is_stream_for_candidate(
"codex",
"openai:responses"
));
assert!(openai_chat_sync_upstream_is_stream_for_candidate(
"codex",
"openai:cli"

View File

@@ -1,13 +0,0 @@
#[path = "decision/payload.rs"]
mod payload;
#[path = "decision/request.rs"]
mod request;
#[path = "decision/support.rs"]
mod support;
pub(super) use self::payload::maybe_build_local_openai_cli_decision_payload_for_candidate;
pub(super) use self::support::{
materialize_local_openai_cli_candidate_attempts, resolve_local_openai_cli_decision_input,
LocalOpenAiCliCandidateAttempt, LocalOpenAiCliDecisionInput,
};
pub(super) use crate::ai_pipeline::LocalOpenAiCliSpec;

View File

@@ -1,5 +1,5 @@
mod chat;
mod cli;
mod responses;
pub(crate) use crate::ai_pipeline::{
copy_request_number_field, copy_request_number_field_as,
@@ -12,9 +12,9 @@ pub(crate) use chat::{
maybe_build_stream_local_decision_payload, maybe_build_sync_local_decision_payload,
set_local_openai_chat_execution_exhausted_diagnostic,
};
pub(crate) use cli::{
build_local_openai_cli_stream_plan_and_reports_for_kind,
build_local_openai_cli_sync_plan_and_reports_for_kind,
maybe_build_stream_local_openai_cli_decision_payload,
maybe_build_sync_local_openai_cli_decision_payload,
pub(crate) use responses::{
build_local_openai_responses_stream_plan_and_reports_for_kind,
build_local_openai_responses_sync_plan_and_reports_for_kind,
maybe_build_stream_local_openai_responses_decision_payload,
maybe_build_sync_local_openai_responses_decision_payload,
};

View File

@@ -4,8 +4,8 @@ mod stream;
mod sync;
pub(crate) use self::stream::{
build_openai_chat_stream_plan_from_decision, build_openai_cli_stream_plan_from_decision,
build_openai_chat_stream_plan_from_decision, build_openai_responses_stream_plan_from_decision,
};
pub(crate) use self::sync::{
build_openai_chat_sync_plan_from_decision, build_openai_cli_sync_plan_from_decision,
build_openai_chat_sync_plan_from_decision, build_openai_responses_sync_plan_from_decision,
};

View File

@@ -10,7 +10,7 @@ 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};
use crate::ai_pipeline::transport::url::{build_openai_chat_url, build_openai_responses_url};
use crate::{GatewayControlSyncDecisionResponse, GatewayError};
pub(crate) fn build_openai_chat_stream_plan_from_decision(
@@ -165,7 +165,7 @@ pub(crate) fn build_openai_chat_stream_plan_from_decision(
}))
}
pub(crate) fn build_openai_cli_stream_plan_from_decision(
pub(crate) fn build_openai_responses_stream_plan_from_decision(
parts: &http::request::Parts,
_body_json: &serde_json::Value,
payload: GatewayControlSyncDecisionResponse,
@@ -207,7 +207,7 @@ pub(crate) fn build_openai_cli_stream_plan_from_decision(
return Ok(None);
};
(
build_openai_cli_url(&upstream_base_url, parts.uri.query(), compact),
build_openai_responses_url(&upstream_base_url, parts.uri.query(), compact),
"upstream_base_url",
)
};
@@ -264,7 +264,7 @@ pub(crate) fn build_openai_cli_stream_plan_from_decision(
};
debug!(
event_name = "local_openai_cli_stream_plan_built",
event_name = "local_openai_responses_stream_plan_built",
log_type = "debug",
request_id = %plan.request_id,
candidate_id = ?plan.candidate_id,
@@ -281,7 +281,7 @@ pub(crate) fn build_openai_cli_stream_plan_from_decision(
provider_api_format = %plan.provider_api_format,
upstream_is_stream = payload.upstream_is_stream,
compact,
"gateway built local openai cli stream execution plan"
"gateway built local openai responses stream execution plan"
);
Ok(Some(LocalStreamPlanAndReport {
@@ -298,7 +298,8 @@ mod tests {
use serde_json::{json, Value};
use super::{
build_openai_chat_stream_plan_from_decision, build_openai_cli_stream_plan_from_decision,
build_openai_chat_stream_plan_from_decision,
build_openai_responses_stream_plan_from_decision,
};
use crate::GatewayControlSyncDecisionResponse;
@@ -311,10 +312,10 @@ mod tests {
.collect()
}
fn sample_cli_payload() -> GatewayControlSyncDecisionResponse {
fn sample_responses_payload() -> GatewayControlSyncDecisionResponse {
GatewayControlSyncDecisionResponse {
action: "stream".to_string(),
decision_kind: Some("openai_cli_stream".to_string()),
decision_kind: Some("openai_responses_stream".to_string()),
execution_strategy: None,
conversion_mode: None,
request_id: Some("req_123".to_string()),
@@ -328,10 +329,10 @@ mod tests {
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()),
provider_api_format: Some("openai:responses".to_string()),
client_api_format: Some("openai:responses".to_string()),
provider_contract: Some("openai:responses".to_string()),
client_contract: Some("openai:responses".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()),
@@ -359,25 +360,27 @@ mod tests {
tls_profile: None,
timeouts: None,
upstream_is_stream: true,
report_kind: Some("openai_cli_stream_success".to_string()),
report_kind: Some("openai_responses_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() {
fn build_openai_responses_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 payload = sample_responses_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 built =
build_openai_responses_stream_plan_from_decision(&parts, &json!({}), payload, false)
.expect("plan build should succeed")
.expect("plan should be produced");
let plan_body = built
.plan
.body

View File

@@ -9,7 +9,7 @@ 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};
use crate::ai_pipeline::transport::url::{build_openai_chat_url, build_openai_responses_url};
use crate::{GatewayControlSyncDecisionResponse, GatewayError};
pub(crate) fn build_openai_chat_sync_plan_from_decision(
@@ -169,7 +169,7 @@ pub(crate) fn build_openai_chat_sync_plan_from_decision(
}))
}
pub(crate) fn build_openai_cli_sync_plan_from_decision(
pub(crate) fn build_openai_responses_sync_plan_from_decision(
parts: &http::request::Parts,
_body_json: &serde_json::Value,
payload: GatewayControlSyncDecisionResponse,
@@ -211,7 +211,7 @@ pub(crate) fn build_openai_cli_sync_plan_from_decision(
return Ok(None);
};
(
build_openai_cli_url(&upstream_base_url, parts.uri.query(), compact),
build_openai_responses_url(&upstream_base_url, parts.uri.query(), compact),
"upstream_base_url",
)
};
@@ -257,7 +257,7 @@ pub(crate) fn build_openai_cli_sync_plan_from_decision(
};
debug!(
event_name = "local_openai_cli_sync_plan_built",
event_name = "local_openai_responses_sync_plan_built",
log_type = "debug",
request_id = %plan.request_id,
candidate_id = ?plan.candidate_id,
@@ -274,7 +274,7 @@ pub(crate) fn build_openai_cli_sync_plan_from_decision(
provider_api_format = %plan.provider_api_format,
upstream_is_stream = payload.upstream_is_stream,
compact,
"gateway built local openai cli sync execution plan"
"gateway built local openai responses sync execution plan"
);
Ok(Some(LocalSyncPlanAndReport {
@@ -291,7 +291,7 @@ mod tests {
use serde_json::{json, Value};
use super::{
build_openai_chat_sync_plan_from_decision, build_openai_cli_sync_plan_from_decision,
build_openai_chat_sync_plan_from_decision, build_openai_responses_sync_plan_from_decision,
};
use crate::GatewayControlSyncDecisionResponse;
@@ -304,10 +304,10 @@ mod tests {
.collect()
}
fn sample_cli_payload() -> GatewayControlSyncDecisionResponse {
fn sample_responses_payload() -> GatewayControlSyncDecisionResponse {
GatewayControlSyncDecisionResponse {
action: "sync".to_string(),
decision_kind: Some("openai_cli_sync".to_string()),
decision_kind: Some("openai_responses_sync".to_string()),
execution_strategy: None,
conversion_mode: None,
request_id: Some("req_123".to_string()),
@@ -321,10 +321,10 @@ mod tests {
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()),
provider_api_format: Some("openai:responses".to_string()),
client_api_format: Some("openai:responses".to_string()),
provider_contract: Some("openai:responses".to_string()),
client_contract: Some("openai:responses".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()),
@@ -352,25 +352,26 @@ mod tests {
tls_profile: None,
timeouts: None,
upstream_is_stream: true,
report_kind: Some("openai_cli_sync_success".to_string()),
report_kind: Some("openai_responses_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() {
fn build_openai_responses_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 payload = sample_responses_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 built =
build_openai_responses_sync_plan_from_decision(&parts, &json!({}), payload, false)
.expect("plan build should succeed")
.expect("plan should be produced");
let plan_body = built
.plan
.body

View File

@@ -0,0 +1,14 @@
#[path = "decision/payload.rs"]
mod payload;
#[path = "decision/request.rs"]
mod request;
#[path = "decision/support.rs"]
mod support;
pub(super) use self::payload::maybe_build_local_openai_responses_decision_payload_for_candidate;
pub(super) use self::support::{
materialize_local_openai_responses_candidate_attempts,
resolve_local_openai_responses_decision_input, LocalOpenAiResponsesCandidateAttempt,
LocalOpenAiResponsesDecisionInput,
};
pub(super) use crate::ai_pipeline::LocalOpenAiResponsesSpec;

View File

@@ -9,7 +9,7 @@ use crate::ai_pipeline::planner::report_context::{
build_local_execution_report_context, insert_provider_stream_event_api_format,
LocalExecutionReportContextParts,
};
use crate::ai_pipeline::planner::spec_metadata::local_openai_cli_spec_metadata;
use crate::ai_pipeline::planner::spec_metadata::local_openai_responses_spec_metadata;
use crate::ai_pipeline::transport::{
resolve_transport_execution_timeouts, resolve_transport_tls_profile,
};
@@ -18,28 +18,28 @@ use crate::{
GatewayControlSyncDecisionResponse,
};
use super::request::resolve_local_openai_cli_candidate_payload_parts;
use super::support::{LocalOpenAiCliCandidateAttempt, LocalOpenAiCliDecisionInput};
use super::LocalOpenAiCliSpec;
use super::request::resolve_local_openai_responses_candidate_payload_parts;
use super::support::{LocalOpenAiResponsesCandidateAttempt, LocalOpenAiResponsesDecisionInput};
use super::LocalOpenAiResponsesSpec;
pub(crate) async fn maybe_build_local_openai_cli_decision_payload_for_candidate(
pub(crate) async fn maybe_build_local_openai_responses_decision_payload_for_candidate(
state: &AppState,
parts: &http::request::Parts,
trace_id: &str,
body_json: &serde_json::Value,
input: &LocalOpenAiCliDecisionInput,
attempt: LocalOpenAiCliCandidateAttempt,
spec: LocalOpenAiCliSpec,
input: &LocalOpenAiResponsesDecisionInput,
attempt: LocalOpenAiResponsesCandidateAttempt,
spec: LocalOpenAiResponsesSpec,
) -> Option<GatewayControlSyncDecisionResponse> {
let spec_metadata = local_openai_cli_spec_metadata(spec);
let spec_metadata = local_openai_responses_spec_metadata(spec);
let attempt_identity = attempt.attempt_identity();
let LocalOpenAiCliCandidateAttempt {
let LocalOpenAiResponsesCandidateAttempt {
eligible,
candidate_index,
candidate_id,
..
} = attempt;
let resolved = resolve_local_openai_cli_candidate_payload_parts(
let resolved = resolve_local_openai_responses_candidate_payload_parts(
state,
parts,
trace_id,
@@ -127,7 +127,7 @@ pub(crate) async fn maybe_build_local_openai_cli_decision_payload_for_candidate(
);
debug!(
event_name = "local_openai_cli_decision_payload_built",
event_name = "local_openai_responses_decision_payload_built",
log_type = "debug",
trace_id = %trace_id,
candidate_id = %candidate_id,
@@ -147,9 +147,9 @@ pub(crate) async fn maybe_build_local_openai_cli_decision_payload_for_candidate(
upstream_url = %resolved.upstream_url,
upstream_is_stream = resolved.upstream_is_stream,
has_envelope = resolved.envelope_name.is_some(),
"gateway built local openai cli decision payload"
"gateway built local openai responses decision payload"
);
let super::request::LocalOpenAiCliCandidatePayloadParts {
let super::request::LocalOpenAiResponsesCandidatePayloadParts {
auth_header,
auth_value,
mapped_model,

View File

@@ -10,11 +10,11 @@ use crate::ai_pipeline::planner::candidate_preparation::{
prepare_header_authenticated_candidate, OauthPreparationContext,
};
use crate::ai_pipeline::planner::common::force_upstream_streaming_for_provider;
use crate::ai_pipeline::planner::spec_metadata::local_openai_cli_spec_metadata;
use crate::ai_pipeline::planner::spec_metadata::local_openai_responses_spec_metadata;
use crate::ai_pipeline::planner::standard::{
apply_codex_openai_cli_special_headers, build_cross_format_openai_cli_request_body,
build_cross_format_openai_cli_upstream_url, build_local_openai_cli_request_body,
build_local_openai_cli_upstream_url, request_body_build_failure_extra_data,
apply_codex_openai_responses_special_headers, build_cross_format_openai_responses_request_body,
build_cross_format_openai_responses_upstream_url, build_local_openai_responses_request_body,
build_local_openai_responses_upstream_url, request_body_build_failure_extra_data,
};
use crate::ai_pipeline::transport::antigravity::{
build_antigravity_safe_v1internal_request, build_antigravity_static_identity_headers,
@@ -41,15 +41,16 @@ use crate::ai_pipeline::{
use crate::AppState;
use super::support::{
mark_skipped_local_openai_cli_candidate,
mark_skipped_local_openai_cli_candidate_with_extra_data,
mark_skipped_local_openai_cli_candidate_with_failure_diagnostic, LocalOpenAiCliDecisionInput,
mark_skipped_local_openai_responses_candidate,
mark_skipped_local_openai_responses_candidate_with_extra_data,
mark_skipped_local_openai_responses_candidate_with_failure_diagnostic,
LocalOpenAiResponsesDecisionInput,
};
use super::LocalOpenAiCliSpec;
use super::LocalOpenAiResponsesSpec;
const ANTIGRAVITY_ENVELOPE_NAME: &str = "antigravity:v1internal";
pub(crate) struct LocalOpenAiCliCandidatePayloadParts {
pub(crate) struct LocalOpenAiResponsesCandidatePayloadParts {
pub(super) auth_header: String,
pub(super) auth_value: String,
pub(super) mapped_model: String,
@@ -66,18 +67,18 @@ pub(crate) struct LocalOpenAiCliCandidatePayloadParts {
}
#[allow(clippy::too_many_arguments)]
pub(crate) async fn resolve_local_openai_cli_candidate_payload_parts(
pub(crate) async fn resolve_local_openai_responses_candidate_payload_parts(
state: &AppState,
parts: &http::request::Parts,
trace_id: &str,
body_json: &serde_json::Value,
input: &LocalOpenAiCliDecisionInput,
input: &LocalOpenAiResponsesDecisionInput,
eligible: &EligibleLocalExecutionCandidate,
candidate_index: u32,
candidate_id: &str,
spec: LocalOpenAiCliSpec,
) -> Option<LocalOpenAiCliCandidatePayloadParts> {
let spec_metadata = local_openai_cli_spec_metadata(spec);
spec: LocalOpenAiResponsesSpec,
) -> Option<LocalOpenAiResponsesCandidatePayloadParts> {
let spec_metadata = local_openai_responses_spec_metadata(spec);
let client_api_format = spec_metadata.api_format.trim().to_ascii_lowercase();
let planner_state = PlannerAppState::new(state);
let candidate = &eligible.candidate;
@@ -95,7 +96,7 @@ pub(crate) async fn resolve_local_openai_cli_candidate_payload_parts(
.eq_ignore_ascii_case("kiro")
&& provider_api_format.eq_ignore_ascii_case("claude:cli");
let same_format = provider_api_format == client_api_format;
let same_format = api_format_alias_matches(provider_api_format, &client_api_format);
let conversion_kind = request_conversion_kind(spec_metadata.api_format, provider_api_format);
let transport_unsupported_reason = if same_format && is_kiro_claude_cli {
local_kiro_request_transport_unsupported_reason_with_network(transport)
@@ -113,7 +114,7 @@ pub(crate) async fn resolve_local_openai_cli_candidate_payload_parts(
}
};
if let Some(skip_reason) = transport_unsupported_reason {
mark_skipped_local_openai_cli_candidate(
mark_skipped_local_openai_responses_candidate(
state,
input,
trace_id,
@@ -129,7 +130,7 @@ pub(crate) async fn resolve_local_openai_cli_candidate_payload_parts(
let oauth_context = OauthPreparationContext {
trace_id,
api_format: provider_api_format,
operation: "openai_cli_candidate_request",
operation: "openai_responses_candidate_request",
};
let kiro_auth = if is_kiro_claude_cli {
match crate::ai_pipeline::planner::candidate_preparation::resolve_candidate_oauth_auth(
@@ -141,7 +142,7 @@ pub(crate) async fn resolve_local_openai_cli_candidate_payload_parts(
{
Some(LocalResolvedOAuthRequestAuth::Kiro(auth)) => Some(auth),
_ => {
mark_skipped_local_openai_cli_candidate(
mark_skipped_local_openai_responses_candidate(
state,
input,
trace_id,
@@ -164,7 +165,9 @@ pub(crate) async fn resolve_local_openai_cli_candidate_payload_parts(
match provider_api_format {
"gemini:cli" => resolve_local_gemini_auth(transport),
"claude:cli" => resolve_local_standard_auth(transport),
"openai:cli" | "openai:compact" => resolve_local_openai_bearer_auth(transport),
"openai:responses" | "openai:responses:compact" | "openai:cli" | "openai:compact" => {
resolve_local_openai_bearer_auth(transport)
}
_ => None,
}
} else {
@@ -177,7 +180,7 @@ pub(crate) async fn resolve_local_openai_cli_candidate_payload_parts(
) {
Ok(mapped_model) => mapped_model,
Err(skip_reason) => {
mark_skipped_local_openai_cli_candidate(
mark_skipped_local_openai_responses_candidate(
state,
input,
trace_id,
@@ -207,7 +210,7 @@ pub(crate) async fn resolve_local_openai_cli_candidate_payload_parts(
{
Ok(prepared) => prepared,
Err(skip_reason) => {
mark_skipped_local_openai_cli_candidate(
mark_skipped_local_openai_responses_candidate(
state,
input,
trace_id,
@@ -233,7 +236,7 @@ pub(crate) async fn resolve_local_openai_cli_candidate_payload_parts(
provider_api_format,
);
let Some(base_provider_request_body) = (if needs_bidirectional_conversion {
build_cross_format_openai_cli_request_body(
build_cross_format_openai_responses_request_body(
body_json,
&mapped_model,
spec_metadata.api_format,
@@ -248,7 +251,7 @@ pub(crate) async fn resolve_local_openai_cli_candidate_payload_parts(
Some(input.auth_context.api_key_id.as_str()),
)
} else {
build_local_openai_cli_request_body(
build_local_openai_responses_request_body(
body_json,
&mapped_model,
upstream_is_stream,
@@ -262,7 +265,7 @@ pub(crate) async fn resolve_local_openai_cli_candidate_payload_parts(
Some(input.auth_context.api_key_id.as_str()),
)
}) else {
mark_skipped_local_openai_cli_candidate_with_extra_data(
mark_skipped_local_openai_responses_candidate_with_extra_data(
state,
input,
trace_id,
@@ -287,7 +290,7 @@ pub(crate) async fn resolve_local_openai_cli_candidate_payload_parts(
) {
AntigravityRequestSideSupport::Supported(spec) => Some(spec.auth),
AntigravityRequestSideSupport::Unsupported(_) => {
mark_skipped_local_openai_cli_candidate(
mark_skipped_local_openai_responses_candidate(
state,
input,
trace_id,
@@ -313,7 +316,7 @@ pub(crate) async fn resolve_local_openai_cli_candidate_payload_parts(
) {
AntigravityRequestEnvelopeSupport::Supported(envelope) => envelope,
AntigravityRequestEnvelopeSupport::Unsupported(_) => {
mark_skipped_local_openai_cli_candidate_with_failure_diagnostic(
mark_skipped_local_openai_responses_candidate_with_failure_diagnostic(
state,
input,
trace_id,
@@ -324,7 +327,7 @@ pub(crate) async fn resolve_local_openai_cli_candidate_payload_parts(
CandidateFailureDiagnostic::envelope_build_failed(
spec_metadata.api_format,
provider_api_format,
"openai_cli_antigravity_envelope",
"openai_responses_antigravity_envelope",
),
)
.await;
@@ -336,7 +339,7 @@ pub(crate) async fn resolve_local_openai_cli_candidate_payload_parts(
};
if let Some(kiro_auth) = kiro_auth.as_ref() {
return build_kiro_openai_cli_payload_parts(
return build_kiro_openai_responses_payload_parts(
state,
parts,
trace_id,
@@ -360,7 +363,7 @@ pub(crate) async fn resolve_local_openai_cli_candidate_payload_parts(
}
let Some(upstream_url) = (if needs_bidirectional_conversion {
build_cross_format_openai_cli_upstream_url(
build_cross_format_openai_responses_upstream_url(
parts,
transport,
&mapped_model,
@@ -369,13 +372,13 @@ pub(crate) async fn resolve_local_openai_cli_candidate_payload_parts(
upstream_is_stream,
)
} else {
build_local_openai_cli_upstream_url(
build_local_openai_responses_upstream_url(
parts,
transport,
provider_api_format == "openai:compact",
api_format_alias_matches(provider_api_format, "openai:responses:compact"),
)
}) else {
mark_skipped_local_openai_cli_candidate_with_failure_diagnostic(
mark_skipped_local_openai_responses_candidate_with_failure_diagnostic(
state,
input,
trace_id,
@@ -386,7 +389,7 @@ pub(crate) async fn resolve_local_openai_cli_candidate_payload_parts(
CandidateFailureDiagnostic::upstream_url_missing(
spec_metadata.api_format,
provider_api_format,
"openai_cli_url",
"openai_responses_url",
),
)
.await;
@@ -435,7 +438,7 @@ pub(crate) async fn resolve_local_openai_cli_candidate_payload_parts(
&provider_request_body,
Some(body_json),
) {
mark_skipped_local_openai_cli_candidate_with_failure_diagnostic(
mark_skipped_local_openai_responses_candidate_with_failure_diagnostic(
state,
input,
trace_id,
@@ -446,13 +449,13 @@ pub(crate) async fn resolve_local_openai_cli_candidate_payload_parts(
CandidateFailureDiagnostic::header_rules_apply_failed(
spec_metadata.api_format,
provider_api_format,
"openai_cli_headers",
"openai_responses_headers",
),
)
.await;
return None;
}
apply_codex_openai_cli_special_headers(
apply_codex_openai_responses_special_headers(
&mut provider_request_headers,
&provider_request_body,
&parts.headers,
@@ -486,7 +489,7 @@ pub(crate) async fn resolve_local_openai_cli_candidate_payload_parts(
};
debug!(
event_name = "local_openai_cli_upstream_url_resolved",
event_name = "local_openai_responses_upstream_url_resolved",
log_type = "debug",
trace_id = %trace_id,
candidate_id = %candidate_id,
@@ -506,10 +509,10 @@ pub(crate) async fn resolve_local_openai_cli_candidate_payload_parts(
mapped_model = %mapped_model,
upstream_url = %upstream_url,
upstream_is_stream,
"gateway resolved local openai cli upstream url"
"gateway resolved local openai responses upstream url"
);
Some(LocalOpenAiCliCandidatePayloadParts {
Some(LocalOpenAiResponsesCandidatePayloadParts {
auth_header,
auth_value,
mapped_model,
@@ -531,13 +534,25 @@ pub(crate) async fn resolve_local_openai_cli_candidate_payload_parts(
})
}
fn api_format_alias_matches(left: &str, right: &str) -> bool {
normalize_api_format_alias(left) == normalize_api_format_alias(right)
}
fn normalize_api_format_alias(value: &str) -> String {
match value.trim().to_ascii_lowercase().as_str() {
"openai:cli" => "openai:responses".to_string(),
"openai:compact" => "openai:responses:compact".to_string(),
other => other.to_string(),
}
}
#[allow(clippy::too_many_arguments)]
async fn build_kiro_openai_cli_payload_parts(
async fn build_kiro_openai_responses_payload_parts(
state: &AppState,
parts: &http::request::Parts,
trace_id: &str,
original_body_json: &serde_json::Value,
input: &LocalOpenAiCliDecisionInput,
input: &LocalOpenAiResponsesDecisionInput,
eligible: &EligibleLocalExecutionCandidate,
candidate_index: u32,
candidate_id: &str,
@@ -551,7 +566,7 @@ async fn build_kiro_openai_cli_payload_parts(
upstream_is_stream: bool,
needs_bidirectional_conversion: bool,
kiro_auth: &KiroRequestAuth,
) -> Option<LocalOpenAiCliCandidatePayloadParts> {
) -> Option<LocalOpenAiResponsesCandidatePayloadParts> {
let candidate = &eligible.candidate;
let provider_request_body = match build_kiro_provider_request_body(
&claude_request_body,
@@ -561,7 +576,7 @@ async fn build_kiro_openai_cli_payload_parts(
) {
Some(body) => body,
None => {
mark_skipped_local_openai_cli_candidate_with_failure_diagnostic(
mark_skipped_local_openai_responses_candidate_with_failure_diagnostic(
state,
input,
trace_id,
@@ -572,7 +587,7 @@ async fn build_kiro_openai_cli_payload_parts(
CandidateFailureDiagnostic::envelope_build_failed(
client_api_format,
provider_api_format,
"openai_cli_kiro_envelope",
"openai_responses_kiro_envelope",
),
)
.await;
@@ -589,7 +604,7 @@ async fn build_kiro_openai_cli_payload_parts(
) {
Some(url) => url,
None => {
mark_skipped_local_openai_cli_candidate_with_failure_diagnostic(
mark_skipped_local_openai_responses_candidate_with_failure_diagnostic(
state,
input,
trace_id,
@@ -600,7 +615,7 @@ async fn build_kiro_openai_cli_payload_parts(
CandidateFailureDiagnostic::upstream_url_missing(
client_api_format,
provider_api_format,
"openai_cli_kiro_url",
"openai_responses_kiro_url",
),
)
.await;
@@ -619,7 +634,7 @@ async fn build_kiro_openai_cli_payload_parts(
}) {
Some(headers) => headers,
None => {
mark_skipped_local_openai_cli_candidate_with_failure_diagnostic(
mark_skipped_local_openai_responses_candidate_with_failure_diagnostic(
state,
input,
trace_id,
@@ -630,7 +645,7 @@ async fn build_kiro_openai_cli_payload_parts(
CandidateFailureDiagnostic::header_rules_apply_failed(
client_api_format,
provider_api_format,
"openai_cli_kiro_headers",
"openai_responses_kiro_headers",
),
)
.await;
@@ -649,7 +664,7 @@ async fn build_kiro_openai_cli_payload_parts(
};
debug!(
event_name = "local_openai_cli_kiro_upstream_url_resolved",
event_name = "local_openai_responses_kiro_upstream_url_resolved",
log_type = "debug",
trace_id = %trace_id,
candidate_id = %candidate_id,
@@ -664,10 +679,10 @@ async fn build_kiro_openai_cli_payload_parts(
conversion_mode = conversion_mode.as_str(),
upstream_url = %upstream_url,
upstream_is_stream,
"gateway resolved local openai cli kiro upstream url"
"gateway resolved local openai responses kiro upstream url"
);
Some(LocalOpenAiCliCandidatePayloadParts {
Some(LocalOpenAiResponsesCandidatePayloadParts {
auth_header,
auth_value,
mapped_model,

View File

@@ -30,7 +30,7 @@ use crate::ai_pipeline::planner::materialization_policy::{
build_local_candidate_persistence_policy, LocalCandidatePersistencePolicyKind,
};
use crate::ai_pipeline::planner::runtime_miss::set_local_runtime_miss_diagnostic_reason;
use crate::ai_pipeline::planner::spec_metadata::local_openai_cli_spec_metadata;
use crate::ai_pipeline::planner::spec_metadata::local_openai_responses_spec_metadata;
use crate::ai_pipeline::planner::CandidateFailureDiagnostic;
use crate::ai_pipeline::PlannerAppState;
use crate::ai_pipeline::{
@@ -40,25 +40,25 @@ use crate::ai_pipeline::{
use crate::clock::current_unix_secs;
use crate::{AppState, GatewayError};
use super::LocalOpenAiCliSpec;
use super::LocalOpenAiResponsesSpec;
pub(crate) use crate::ai_pipeline::planner::candidate_materialization::LocalExecutionCandidateAttempt as LocalOpenAiCliCandidateAttempt;
pub(crate) use crate::ai_pipeline::planner::decision_input::LocalRequestedModelDecisionInput as LocalOpenAiCliDecisionInput;
pub(crate) use crate::ai_pipeline::planner::candidate_materialization::LocalExecutionCandidateAttempt as LocalOpenAiResponsesCandidateAttempt;
pub(crate) use crate::ai_pipeline::planner::decision_input::LocalRequestedModelDecisionInput as LocalOpenAiResponsesDecisionInput;
pub(crate) async fn resolve_local_openai_cli_decision_input(
pub(crate) async fn resolve_local_openai_responses_decision_input(
state: &AppState,
trace_id: &str,
decision: &GatewayControlDecision,
body_json: &serde_json::Value,
plan_kind: &str,
) -> Option<LocalOpenAiCliDecisionInput> {
) -> Option<LocalOpenAiResponsesDecisionInput> {
let Some(auth_context) = resolve_local_decision_execution_runtime_auth_context(decision) else {
warn!(
trace_id = %trace_id,
route_class = ?decision.route_class,
route_family = ?decision.route_family,
route_kind = ?decision.route_kind,
"gateway local openai cli decision skipped: missing_auth_context"
"gateway local openai responses decision skipped: missing_auth_context"
);
set_local_runtime_miss_diagnostic_reason(
state,
@@ -74,7 +74,7 @@ pub(crate) async fn resolve_local_openai_cli_decision_input(
let Some(requested_model) = extract_standard_requested_model(body_json) else {
warn!(
trace_id = %trace_id,
"gateway local openai cli decision skipped: missing_requested_model"
"gateway local openai responses decision skipped: missing_requested_model"
);
set_local_runtime_miss_diagnostic_reason(
state,
@@ -101,7 +101,7 @@ pub(crate) async fn resolve_local_openai_cli_decision_input(
trace_id = %trace_id,
user_id = %auth_context.user_id,
api_key_id = %auth_context.api_key_id,
"gateway local openai cli decision skipped: auth_snapshot_missing"
"gateway local openai responses decision skipped: auth_snapshot_missing"
);
set_local_runtime_miss_diagnostic_reason(
state,
@@ -117,7 +117,7 @@ pub(crate) async fn resolve_local_openai_cli_decision_input(
warn!(
trace_id = %trace_id,
error = ?err,
"gateway local openai cli decision auth snapshot read failed"
"gateway local openai responses decision auth snapshot read failed"
);
set_local_runtime_miss_diagnostic_reason(
state,
@@ -137,14 +137,14 @@ pub(crate) async fn resolve_local_openai_cli_decision_input(
))
}
pub(crate) async fn materialize_local_openai_cli_candidate_attempts(
pub(crate) async fn materialize_local_openai_responses_candidate_attempts(
state: &AppState,
trace_id: &str,
input: &LocalOpenAiCliDecisionInput,
input: &LocalOpenAiResponsesDecisionInput,
body_json: &serde_json::Value,
spec: LocalOpenAiCliSpec,
) -> Result<(Vec<LocalOpenAiCliCandidateAttempt>, usize), GatewayError> {
let spec_metadata = local_openai_cli_spec_metadata(spec);
spec: LocalOpenAiResponsesSpec,
) -> Result<(Vec<LocalOpenAiResponsesCandidateAttempt>, usize), GatewayError> {
let spec_metadata = local_openai_responses_spec_metadata(spec);
let client_api_format = spec_metadata.api_format.trim().to_ascii_lowercase();
let planner_state = PlannerAppState::new(state);
let sticky_session_token = extract_pool_sticky_session_token(body_json);
@@ -152,7 +152,7 @@ pub(crate) async fn materialize_local_openai_cli_candidate_attempts(
let persistence_policy = build_local_candidate_persistence_policy(
auth_context,
input.required_capabilities.as_ref(),
LocalCandidatePersistencePolicyKind::OpenAiCliDecision,
LocalCandidatePersistencePolicyKind::OpenAiResponsesDecision,
);
let mut seen_candidates = BTreeSet::new();
let mut seen_skipped_candidates = BTreeSet::new();
@@ -161,11 +161,12 @@ pub(crate) async fn materialize_local_openai_cli_candidate_attempts(
for candidate_api_format in
request_candidate_api_formats(spec_metadata.api_format, spec_metadata.require_streaming)
{
let auth_snapshot = if candidate_api_format == spec_metadata.api_format {
Some(&input.auth_snapshot)
} else {
None
};
let auth_snapshot =
if api_format_alias_matches(candidate_api_format, spec_metadata.api_format) {
Some(&input.auth_snapshot)
} else {
None
};
let (mut selected_candidates, skipped_candidates) = planner_state
.list_selectable_candidates_with_skip_reasons(
candidate_api_format,
@@ -337,9 +338,21 @@ pub(crate) async fn materialize_local_openai_cli_candidate_attempts(
Ok((attempts, candidate_count))
}
pub(crate) async fn mark_skipped_local_openai_cli_candidate(
fn api_format_alias_matches(left: &str, right: &str) -> bool {
normalize_api_format_alias(left) == normalize_api_format_alias(right)
}
fn normalize_api_format_alias(value: &str) -> String {
match value.trim().to_ascii_lowercase().as_str() {
"openai:cli" => "openai:responses".to_string(),
"openai:compact" => "openai:responses:compact".to_string(),
other => other.to_string(),
}
}
pub(crate) async fn mark_skipped_local_openai_responses_candidate(
state: &AppState,
input: &LocalOpenAiCliDecisionInput,
input: &LocalOpenAiResponsesDecisionInput,
trace_id: &str,
candidate: &SchedulerMinimalCandidateSelectionCandidate,
candidate_index: u32,
@@ -350,7 +363,7 @@ pub(crate) async fn mark_skipped_local_openai_cli_candidate(
let persistence_policy = build_local_candidate_persistence_policy(
auth_context,
input.required_capabilities.as_ref(),
LocalCandidatePersistencePolicyKind::OpenAiCliDecision,
LocalCandidatePersistencePolicyKind::OpenAiResponsesDecision,
);
mark_skipped_local_execution_candidate(
state,
@@ -365,9 +378,9 @@ pub(crate) async fn mark_skipped_local_openai_cli_candidate(
}
#[allow(clippy::too_many_arguments)]
pub(crate) async fn mark_skipped_local_openai_cli_candidate_with_extra_data(
pub(crate) async fn mark_skipped_local_openai_responses_candidate_with_extra_data(
state: &AppState,
input: &LocalOpenAiCliDecisionInput,
input: &LocalOpenAiResponsesDecisionInput,
trace_id: &str,
candidate: &SchedulerMinimalCandidateSelectionCandidate,
candidate_index: u32,
@@ -379,7 +392,7 @@ pub(crate) async fn mark_skipped_local_openai_cli_candidate_with_extra_data(
let persistence_policy = build_local_candidate_persistence_policy(
auth_context,
input.required_capabilities.as_ref(),
LocalCandidatePersistencePolicyKind::OpenAiCliDecision,
LocalCandidatePersistencePolicyKind::OpenAiResponsesDecision,
);
mark_skipped_local_execution_candidate_with_extra_data(
state,
@@ -395,9 +408,9 @@ pub(crate) async fn mark_skipped_local_openai_cli_candidate_with_extra_data(
}
#[allow(clippy::too_many_arguments)]
pub(crate) async fn mark_skipped_local_openai_cli_candidate_with_failure_diagnostic(
pub(crate) async fn mark_skipped_local_openai_responses_candidate_with_failure_diagnostic(
state: &AppState,
input: &LocalOpenAiCliDecisionInput,
input: &LocalOpenAiResponsesDecisionInput,
trace_id: &str,
candidate: &SchedulerMinimalCandidateSelectionCandidate,
candidate_index: u32,
@@ -409,7 +422,7 @@ pub(crate) async fn mark_skipped_local_openai_cli_candidate_with_failure_diagnos
let persistence_policy = build_local_candidate_persistence_policy(
auth_context,
input.required_capabilities.as_ref(),
LocalCandidatePersistencePolicyKind::OpenAiCliDecision,
LocalCandidatePersistencePolicyKind::OpenAiResponsesDecision,
);
mark_skipped_local_execution_candidate_with_failure_diagnostic(
state,

View File

@@ -8,16 +8,16 @@ mod decision;
mod plans;
use self::decision::{
materialize_local_openai_cli_candidate_attempts,
maybe_build_local_openai_cli_decision_payload_for_candidate,
resolve_local_openai_cli_decision_input,
materialize_local_openai_responses_candidate_attempts,
maybe_build_local_openai_responses_decision_payload_for_candidate,
resolve_local_openai_responses_decision_input,
};
use self::plans::{
build_local_stream_plan_and_reports, build_local_sync_plan_and_reports, resolve_stream_spec,
resolve_sync_spec,
};
pub(crate) async fn build_local_openai_cli_sync_plan_and_reports_for_kind(
pub(crate) async fn build_local_openai_responses_sync_plan_and_reports_for_kind(
state: &AppState,
parts: &http::request::Parts,
trace_id: &str,
@@ -32,7 +32,7 @@ pub(crate) async fn build_local_openai_cli_sync_plan_and_reports_for_kind(
build_local_sync_plan_and_reports(state, parts, trace_id, decision, body_json, spec).await
}
pub(crate) async fn build_local_openai_cli_stream_plan_and_reports_for_kind(
pub(crate) async fn build_local_openai_responses_stream_plan_and_reports_for_kind(
state: &AppState,
parts: &http::request::Parts,
trace_id: &str,
@@ -47,7 +47,7 @@ pub(crate) async fn build_local_openai_cli_stream_plan_and_reports_for_kind(
build_local_stream_plan_and_reports(state, parts, trace_id, decision, body_json, spec).await
}
pub(crate) async fn maybe_build_sync_local_openai_cli_decision_payload(
pub(crate) async fn maybe_build_sync_local_openai_responses_decision_payload(
state: &AppState,
parts: &http::request::Parts,
trace_id: &str,
@@ -59,19 +59,21 @@ pub(crate) async fn maybe_build_sync_local_openai_cli_decision_payload(
return Ok(None);
};
let Some(input) =
resolve_local_openai_cli_decision_input(state, trace_id, decision, body_json, plan_kind)
.await
let Some(input) = resolve_local_openai_responses_decision_input(
state, trace_id, decision, body_json, plan_kind,
)
.await
else {
return Ok(None);
};
let (attempts, _) =
materialize_local_openai_cli_candidate_attempts(state, trace_id, &input, body_json, spec)
.await?;
let (attempts, _) = materialize_local_openai_responses_candidate_attempts(
state, trace_id, &input, body_json, spec,
)
.await?;
for attempt in attempts {
if let Some(payload) = maybe_build_local_openai_cli_decision_payload_for_candidate(
if let Some(payload) = maybe_build_local_openai_responses_decision_payload_for_candidate(
state, parts, trace_id, body_json, &input, attempt, spec,
)
.await
@@ -83,7 +85,7 @@ pub(crate) async fn maybe_build_sync_local_openai_cli_decision_payload(
Ok(None)
}
pub(crate) async fn maybe_build_stream_local_openai_cli_decision_payload(
pub(crate) async fn maybe_build_stream_local_openai_responses_decision_payload(
state: &AppState,
parts: &http::request::Parts,
trace_id: &str,
@@ -95,19 +97,21 @@ pub(crate) async fn maybe_build_stream_local_openai_cli_decision_payload(
return Ok(None);
};
let Some(input) =
resolve_local_openai_cli_decision_input(state, trace_id, decision, body_json, plan_kind)
.await
let Some(input) = resolve_local_openai_responses_decision_input(
state, trace_id, decision, body_json, plan_kind,
)
.await
else {
return Ok(None);
};
let (attempts, _) =
materialize_local_openai_cli_candidate_attempts(state, trace_id, &input, body_json, spec)
.await?;
let (attempts, _) = materialize_local_openai_responses_candidate_attempts(
state, trace_id, &input, body_json, spec,
)
.await?;
for attempt in attempts {
if let Some(payload) = maybe_build_local_openai_cli_decision_payload_for_candidate(
if let Some(payload) = maybe_build_local_openai_responses_decision_payload_for_candidate(
state, parts, trace_id, body_json, &input, attempt, spec,
)
.await

View File

@@ -1,23 +1,24 @@
use tracing::warn;
use super::decision::{
materialize_local_openai_cli_candidate_attempts,
maybe_build_local_openai_cli_decision_payload_for_candidate,
resolve_local_openai_cli_decision_input, LocalOpenAiCliSpec,
materialize_local_openai_responses_candidate_attempts,
maybe_build_local_openai_responses_decision_payload_for_candidate,
resolve_local_openai_responses_decision_input, LocalOpenAiResponsesSpec,
};
use crate::ai_pipeline::planner::plan_builders::{
build_openai_cli_stream_plan_from_decision, build_openai_cli_sync_plan_from_decision,
LocalStreamPlanAndReport, LocalSyncPlanAndReport,
build_openai_responses_stream_plan_from_decision,
build_openai_responses_sync_plan_from_decision, LocalStreamPlanAndReport,
LocalSyncPlanAndReport,
};
use crate::ai_pipeline::planner::runtime_miss::{
apply_local_runtime_candidate_evaluation_progress,
apply_local_runtime_candidate_terminal_reason, set_local_runtime_miss_diagnostic_reason,
};
use crate::ai_pipeline::planner::spec_metadata::local_openai_cli_spec_metadata;
use crate::ai_pipeline::planner::spec_metadata::local_openai_responses_spec_metadata;
use crate::ai_pipeline::GatewayControlDecision;
pub(crate) use crate::ai_pipeline::{
resolve_openai_cli_stream_spec as resolve_stream_spec,
resolve_openai_cli_sync_spec as resolve_sync_spec,
resolve_openai_responses_stream_spec as resolve_stream_spec,
resolve_openai_responses_sync_spec as resolve_sync_spec,
};
use crate::{AppState, GatewayError};
@@ -27,10 +28,10 @@ pub(super) async fn build_local_sync_plan_and_reports(
trace_id: &str,
decision: &GatewayControlDecision,
body_json: &serde_json::Value,
spec: LocalOpenAiCliSpec,
spec: LocalOpenAiResponsesSpec,
) -> Result<Vec<LocalSyncPlanAndReport>, GatewayError> {
let spec_metadata = local_openai_cli_spec_metadata(spec);
let Some(input) = resolve_local_openai_cli_decision_input(
let spec_metadata = local_openai_responses_spec_metadata(spec);
let Some(input) = resolve_local_openai_responses_decision_input(
state,
trace_id,
decision,
@@ -50,9 +51,10 @@ pub(super) async fn build_local_sync_plan_and_reports(
"candidate_evaluation_incomplete",
);
let (attempts, candidate_count) =
materialize_local_openai_cli_candidate_attempts(state, trace_id, &input, body_json, spec)
.await?;
let (attempts, candidate_count) = materialize_local_openai_responses_candidate_attempts(
state, trace_id, &input, body_json, spec,
)
.await?;
apply_local_runtime_candidate_evaluation_progress(state, trace_id, candidate_count);
if candidate_count == 0 {
return Ok(Vec::new());
@@ -60,7 +62,7 @@ pub(super) async fn build_local_sync_plan_and_reports(
let mut plans = Vec::new();
for attempt in attempts {
let Some(payload) = maybe_build_local_openai_cli_decision_payload_for_candidate(
let Some(payload) = maybe_build_local_openai_responses_decision_payload_for_candidate(
state, parts, trace_id, body_json, &input, attempt, spec,
)
.await
@@ -68,7 +70,12 @@ pub(super) async fn build_local_sync_plan_and_reports(
continue;
};
match build_openai_cli_sync_plan_from_decision(parts, body_json, payload, spec.compact) {
match build_openai_responses_sync_plan_from_decision(
parts,
body_json,
payload,
spec.compact,
) {
Ok(Some(value)) => plans.push(value),
Ok(None) => {}
Err(err) => {
@@ -76,7 +83,7 @@ pub(super) async fn build_local_sync_plan_and_reports(
trace_id = %trace_id,
api_format = spec_metadata.api_format,
error = ?err,
"gateway local openai cli sync decision plan build failed"
"gateway local openai responses sync decision plan build failed"
);
}
}
@@ -92,10 +99,10 @@ pub(super) async fn build_local_stream_plan_and_reports(
trace_id: &str,
decision: &GatewayControlDecision,
body_json: &serde_json::Value,
spec: LocalOpenAiCliSpec,
spec: LocalOpenAiResponsesSpec,
) -> Result<Vec<LocalStreamPlanAndReport>, GatewayError> {
let spec_metadata = local_openai_cli_spec_metadata(spec);
let Some(input) = resolve_local_openai_cli_decision_input(
let spec_metadata = local_openai_responses_spec_metadata(spec);
let Some(input) = resolve_local_openai_responses_decision_input(
state,
trace_id,
decision,
@@ -115,9 +122,10 @@ pub(super) async fn build_local_stream_plan_and_reports(
"candidate_evaluation_incomplete",
);
let (attempts, candidate_count) =
materialize_local_openai_cli_candidate_attempts(state, trace_id, &input, body_json, spec)
.await?;
let (attempts, candidate_count) = materialize_local_openai_responses_candidate_attempts(
state, trace_id, &input, body_json, spec,
)
.await?;
apply_local_runtime_candidate_evaluation_progress(state, trace_id, candidate_count);
if candidate_count == 0 {
return Ok(Vec::new());
@@ -125,7 +133,7 @@ pub(super) async fn build_local_stream_plan_and_reports(
let mut plans = Vec::new();
for attempt in attempts {
let Some(payload) = maybe_build_local_openai_cli_decision_payload_for_candidate(
let Some(payload) = maybe_build_local_openai_responses_decision_payload_for_candidate(
state, parts, trace_id, body_json, &input, attempt, spec,
)
.await
@@ -133,7 +141,12 @@ pub(super) async fn build_local_stream_plan_and_reports(
continue;
};
match build_openai_cli_stream_plan_from_decision(parts, body_json, payload, spec.compact) {
match build_openai_responses_stream_plan_from_decision(
parts,
body_json,
payload,
spec.compact,
) {
Ok(Some(value)) => plans.push(value),
Ok(None) => {}
Err(err) => {
@@ -141,7 +154,7 @@ pub(super) async fn build_local_stream_plan_and_reports(
trace_id = %trace_id,
api_format = spec_metadata.api_format,
error = ?err,
"gateway local openai cli stream decision plan build failed"
"gateway local openai responses stream decision plan build failed"
);
}
}

View File

@@ -47,13 +47,13 @@ fn diagnose_request_body_build_failure(
return Some(diagnostic("$", "请求体必须是 JSON object"));
}
if client_api_format == "openai:cli" {
if let Some(diagnostic) = diagnose_openai_cli_request(body_json) {
if is_openai_responses_client_format(client_api_format) {
if let Some(diagnostic) = diagnose_openai_responses_request(body_json) {
return Some(diagnostic);
}
return Some(diagnostic(
"$",
"OpenAI CLI 请求体初步结构检查通过;失败可能发生在后续跨格式转换或 Body 规则应用",
"OpenAI Responses 请求体初步结构检查通过;失败可能发生在后续跨格式转换或 Body 规则应用",
));
}
@@ -70,6 +70,14 @@ fn diagnose_request_body_build_failure(
))
}
fn is_openai_responses_client_format(client_api_format: &str) -> bool {
let normalized = client_api_format.trim().to_ascii_lowercase();
matches!(
normalized.as_str(),
"openai:responses" | "openai:responses:compact" | "openai:cli" | "openai:compact"
)
}
fn diagnose_same_format_provider_request_body_failure(
body_json: &Value,
body_rules: Option<&Value>,
@@ -176,25 +184,26 @@ fn diagnose_openai_chat_cross_format_request(
diagnose_openai_tool_choice(request.get("tool_choice"))
}
fn diagnose_openai_cli_request(body_json: &Value) -> Option<RequestBodyBuildDiagnostic> {
fn diagnose_openai_responses_request(body_json: &Value) -> Option<RequestBodyBuildDiagnostic> {
let request = body_json.as_object()?;
if let Some(diagnostic) =
diagnose_openai_cli_text_content(request.get("instructions"), "$.instructions".to_string())
{
if let Some(diagnostic) = diagnose_openai_responses_text_content(
request.get("instructions"),
"$.instructions".to_string(),
) {
return Some(diagnostic);
}
if let Some(diagnostic) = diagnose_openai_cli_input(request.get("input")) {
if let Some(diagnostic) = diagnose_openai_responses_input(request.get("input")) {
return Some(diagnostic);
}
if let Some(diagnostic) = diagnose_openai_cli_tools(request.get("tools")) {
if let Some(diagnostic) = diagnose_openai_responses_tools(request.get("tools")) {
return Some(diagnostic);
}
diagnose_openai_cli_tool_choice(request.get("tool_choice"))
diagnose_openai_responses_tool_choice(request.get("tool_choice"))
}
fn diagnose_openai_cli_input(input: Option<&Value>) -> Option<RequestBodyBuildDiagnostic> {
fn diagnose_openai_responses_input(input: Option<&Value>) -> Option<RequestBodyBuildDiagnostic> {
let Some(input) = input else {
return None;
};
@@ -209,7 +218,7 @@ fn diagnose_openai_cli_input(input: Option<&Value>) -> Option<RequestBodyBuildDi
let Some(item_object) = item.as_object() else {
return Some(diagnostic(
item_path,
"OpenAI CLI input 数组项必须是 string 或 object",
"OpenAI Responses input 数组项必须是 string 或 object",
));
};
let item_type = item_object
@@ -227,13 +236,13 @@ fn diagnose_openai_cli_input(input: Option<&Value>) -> Option<RequestBodyBuildDi
.trim()
.to_ascii_lowercase();
if role == "system" || role == "developer" {
if let Some(diagnostic) = diagnose_openai_cli_text_content(
if let Some(diagnostic) = diagnose_openai_responses_text_content(
item_object.get("content"),
format!("{item_path}.content"),
) {
return Some(diagnostic);
}
} else if let Some(diagnostic) = diagnose_openai_cli_message_content(
} else if let Some(diagnostic) = diagnose_openai_responses_message_content(
item_object.get("content"),
format!("{item_path}.content"),
) {
@@ -260,12 +269,12 @@ fn diagnose_openai_cli_input(input: Option<&Value>) -> Option<RequestBodyBuildDi
}
_ => Some(diagnostic(
"$.input",
"OpenAI CLI input 必须是 string、array 或 null",
"OpenAI Responses input 必须是 string、array 或 null",
)),
}
}
fn diagnose_openai_cli_text_content(
fn diagnose_openai_responses_text_content(
content: Option<&Value>,
path: String,
) -> Option<RequestBodyBuildDiagnostic> {
@@ -289,7 +298,7 @@ fn diagnose_openai_cli_text_content(
}
}
fn diagnose_openai_cli_message_content(
fn diagnose_openai_responses_message_content(
content: Option<&Value>,
path: String,
) -> Option<RequestBodyBuildDiagnostic> {
@@ -464,7 +473,7 @@ fn diagnose_openai_tools(
None
}
fn diagnose_openai_cli_tools(tools: Option<&Value>) -> Option<RequestBodyBuildDiagnostic> {
fn diagnose_openai_responses_tools(tools: Option<&Value>) -> Option<RequestBodyBuildDiagnostic> {
let Some(tools) = tools else {
return None;
};
@@ -474,7 +483,7 @@ fn diagnose_openai_cli_tools(tools: Option<&Value>) -> Option<RequestBodyBuildDi
for (tool_index, tool) in tool_values.iter().enumerate() {
let tool_path = format!("$.tools[{tool_index}]");
let Some(tool_object) = tool.as_object() else {
return Some(diagnostic(tool_path, "OpenAI CLI tool 必须是 object"));
return Some(diagnostic(tool_path, "OpenAI Responses tool 必须是 object"));
};
let tool_type = tool_object
.get("type")
@@ -496,14 +505,14 @@ fn diagnose_openai_cli_tools(tools: Option<&Value>) -> Option<RequestBodyBuildDi
if !valid_name {
return Some(diagnostic(
format!("{tool_path}.name"),
"OpenAI CLI function tool 必须包含非空 name",
"OpenAI Responses function tool 必须包含非空 name",
));
}
}
None
}
fn diagnose_openai_cli_tool_choice(
fn diagnose_openai_responses_tool_choice(
tool_choice: Option<&Value>,
) -> Option<RequestBodyBuildDiagnostic> {
let Some(Value::Object(object)) = tool_choice else {
@@ -527,7 +536,7 @@ fn diagnose_openai_cli_tool_choice(
} else {
Some(diagnostic(
"$.tool_choice.name",
"OpenAI CLI tool_choice 指定 function 时必须包含非空 name",
"OpenAI Responses tool_choice 指定 function 时必须包含非空 name",
))
}
}
@@ -666,7 +675,7 @@ mod tests {
}
#[test]
fn openai_cli_reports_invalid_function_call_name() {
fn openai_responses_reports_invalid_function_call_name() {
let body = json!({
"model": "gpt-5.4",
"input": [{
@@ -675,8 +684,9 @@ mod tests {
}]
});
let diagnostic = request_body_build_failure_extra_data(&body, "openai:cli", "claude:chat")
.expect("diagnostic");
let diagnostic =
request_body_build_failure_extra_data(&body, "openai:responses", "claude:chat")
.expect("diagnostic");
assert_eq!(
diagnostic["request_body_build_error"]["path"],
@@ -685,15 +695,16 @@ mod tests {
}
#[test]
fn openai_cli_reports_invalid_tool_choice_name() {
fn openai_responses_reports_invalid_tool_choice_name() {
let body = json!({
"model": "gpt-5.4",
"input": "hello",
"tool_choice": { "type": "function" }
});
let diagnostic = request_body_build_failure_extra_data(&body, "openai:cli", "gemini:chat")
.expect("diagnostic");
let diagnostic =
request_body_build_failure_extra_data(&body, "openai:responses", "gemini:chat")
.expect("diagnostic");
assert_eq!(
diagnostic["request_body_build_error"]["path"],

View File

@@ -1,23 +1,24 @@
pub(crate) use aether_ai_pipeline::api::{
aggregate_claude_stream_sync_response, aggregate_gemini_stream_sync_response,
aggregate_openai_chat_stream_sync_response, aggregate_openai_cli_stream_sync_response,
aggregate_openai_chat_stream_sync_response, aggregate_openai_responses_stream_sync_response,
aggregate_standard_chat_stream_sync_response, aggregate_standard_cli_stream_sync_response,
apply_codex_openai_cli_special_body_edits, apply_codex_openai_cli_special_headers,
apply_openai_compact_special_body_edits, augment_sync_report_context,
apply_codex_openai_responses_special_body_edits, apply_codex_openai_responses_special_headers,
apply_openai_responses_compact_special_body_edits, augment_sync_report_context,
build_core_error_body_for_client_format, build_cross_format_openai_chat_request_body,
build_cross_format_openai_cli_request_body, build_generated_tool_call_id,
build_cross_format_openai_responses_request_body, build_generated_tool_call_id,
build_kiro_final_message_sse_events, build_kiro_initial_sse_events,
build_kiro_stream_error_sse_events, build_local_openai_chat_request_body,
build_local_openai_cli_request_body, build_local_success_background_report,
build_local_success_conversion_background_report, build_openai_cli_response,
build_local_openai_responses_request_body, build_local_success_background_report,
build_local_success_conversion_background_report, build_openai_responses_response,
build_standard_request_body, build_standard_request_body_from_canonical,
build_standard_upstream_url, calculate_kiro_context_input_tokens, canonicalize_tool_arguments,
convert_claude_chat_response_to_openai_chat, convert_claude_cli_response_to_openai_cli,
convert_gemini_chat_response_to_openai_chat, convert_gemini_cli_response_to_openai_cli,
convert_claude_chat_response_to_openai_chat, convert_claude_response_to_openai_responses,
convert_gemini_chat_response_to_openai_chat, convert_gemini_response_to_openai_responses,
convert_openai_chat_request_to_claude_request, convert_openai_chat_request_to_gemini_request,
convert_openai_chat_request_to_openai_cli_request, convert_openai_chat_response_to_claude_chat,
convert_openai_chat_response_to_gemini_chat, convert_openai_chat_response_to_openai_cli,
convert_openai_cli_response_to_openai_chat, convert_standard_chat_response,
convert_openai_chat_request_to_openai_responses_request,
convert_openai_chat_response_to_claude_chat, convert_openai_chat_response_to_gemini_chat,
convert_openai_chat_response_to_openai_responses,
convert_openai_responses_response_to_openai_chat, convert_standard_chat_response,
convert_standard_cli_response, copy_request_number_field, copy_request_number_field_as,
core_error_background_report_kind, core_error_default_client_api_format,
core_success_background_report_kind, encode_done_sse, encode_json_sse, encode_kiro_sse_events,
@@ -28,17 +29,18 @@ pub(crate) use aether_ai_pipeline::api::{
kiro_crc32, map_claude_stop_reason, map_openai_reasoning_effort_to_claude_output,
map_openai_reasoning_effort_to_gemini_budget,
maybe_build_openai_chat_cross_format_sync_product_from_normalized_payload,
maybe_build_openai_cli_cross_format_sync_product_from_normalized_payload,
maybe_build_openai_cli_same_family_sync_body_from_normalized_payload,
maybe_build_openai_responses_cross_format_sync_product_from_normalized_payload,
maybe_build_openai_responses_same_family_sync_body_from_normalized_payload,
maybe_build_standard_cross_format_sync_product,
maybe_build_standard_cross_format_sync_product_from_normalized_payload,
maybe_build_standard_same_format_sync_body_from_normalized_payload,
maybe_build_standard_sync_finalize_product_from_normalized_payload,
normalize_claude_request_to_openai_chat_request,
normalize_gemini_request_to_openai_chat_request,
normalize_openai_cli_request_to_openai_chat_request, normalize_provider_private_report_context,
normalize_provider_private_response_value, normalize_standard_request_to_openai_chat_request,
parse_direct_request_body, parse_openai_stop_sequences, parse_openai_tool_result_content,
normalize_openai_responses_request_to_openai_chat_request,
normalize_provider_private_report_context, normalize_provider_private_response_value,
normalize_standard_request_to_openai_chat_request, parse_direct_request_body,
parse_openai_stop_sequences, parse_openai_tool_result_content,
prepare_local_success_response_parts, prepare_local_success_response_parts_owned,
provider_adaptation_allows_sync_finalize_envelope, provider_adaptation_anchor_api_format,
provider_adaptation_descriptor_for_envelope, provider_adaptation_descriptor_for_provider_type,
@@ -55,19 +57,19 @@ pub(crate) use aether_ai_pipeline::api::{
resolve_gemini_files_sync_spec, resolve_gemini_stream_spec, resolve_gemini_sync_spec,
resolve_local_image_stream_spec, resolve_local_image_sync_spec,
resolve_local_same_format_stream_spec, resolve_local_same_format_sync_spec,
resolve_local_video_sync_spec, resolve_openai_chat_max_tokens, resolve_openai_cli_stream_spec,
resolve_openai_cli_sync_spec, stream_body_contains_error_event,
supports_stream_scheduler_decision_kind, supports_sync_scheduler_decision_kind,
sync_chat_response_conversion_kind, sync_cli_response_conversion_kind,
transform_provider_private_stream_line, value_as_u64, CanonicalStreamFrame,
ClaudeClientEmitter, ClaudeProviderState, ExecutionRuntimeAuthContext,
resolve_local_video_sync_spec, resolve_openai_chat_max_tokens,
resolve_openai_responses_stream_spec, resolve_openai_responses_sync_spec,
stream_body_contains_error_event, supports_stream_scheduler_decision_kind,
supports_sync_scheduler_decision_kind, sync_chat_response_conversion_kind,
sync_cli_response_conversion_kind, transform_provider_private_stream_line, value_as_u64,
CanonicalStreamFrame, ClaudeClientEmitter, ClaudeProviderState, ExecutionRuntimeAuthContext,
FinalizeStreamRewriteMode, GatewayControlPlanRequest, GatewayControlPlanResponse,
GatewayControlSyncDecisionResponse, GeminiClientEmitter, GeminiProviderState,
LocalCoreSyncErrorKind, LocalGeminiFilesSpec, LocalOpenAiCliSpec, LocalOpenAiImageSpec,
LocalCoreSyncErrorKind, LocalGeminiFilesSpec, LocalOpenAiImageSpec, LocalOpenAiResponsesSpec,
LocalSameFormatProviderFamily, LocalSameFormatProviderSpec, LocalStandardSourceFamily,
LocalStandardSourceMode, LocalStandardSpec, LocalStreamPlanAndReport, LocalSyncPlanAndReport,
LocalVideoCreateFamily, LocalVideoCreateSpec, OpenAIChatClientEmitter, OpenAIChatProviderState,
OpenAICliClientEmitter, OpenAICliProviderState, PipelineFinalizeError,
OpenAIResponsesClientEmitter, OpenAIResponsesProviderState, PipelineFinalizeError,
ProviderAdaptationDescriptor, ProviderAdaptationSurface, RequestConversionKind,
StandardCrossFormatSyncProduct, StandardSyncFinalizeNormalizedProduct,
StreamingStandardFormatMatrix, SyncChatResponseConversionKind, SyncCliResponseConversionKind,
@@ -102,7 +104,14 @@ pub(crate) use aether_ai_pipeline::api::{
OPENAI_COMPACT_SYNC_PLAN_KIND, OPENAI_IMAGE_STREAM_PLAN_KIND,
OPENAI_IMAGE_STREAM_SUCCESS_REPORT_KIND, OPENAI_IMAGE_SYNC_FINALIZE_REPORT_KIND,
OPENAI_IMAGE_SYNC_PLAN_KIND, OPENAI_IMAGE_SYNC_SUCCESS_REPORT_KIND,
OPENAI_VIDEO_CANCEL_SYNC_PLAN_KIND, OPENAI_VIDEO_CONTENT_PLAN_KIND,
OPENAI_VIDEO_CREATE_SYNC_FINALIZE_REPORT_KIND, OPENAI_VIDEO_CREATE_SYNC_PLAN_KIND,
OPENAI_VIDEO_DELETE_SYNC_PLAN_KIND, OPENAI_VIDEO_REMIX_SYNC_PLAN_KIND,
OPENAI_RESPONSES_COMPACT_STREAM_PLAN_KIND, OPENAI_RESPONSES_COMPACT_STREAM_SUCCESS_REPORT_KIND,
OPENAI_RESPONSES_COMPACT_SYNC_ERROR_REPORT_KIND,
OPENAI_RESPONSES_COMPACT_SYNC_FINALIZE_REPORT_KIND, OPENAI_RESPONSES_COMPACT_SYNC_PLAN_KIND,
OPENAI_RESPONSES_COMPACT_SYNC_SUCCESS_REPORT_KIND, OPENAI_RESPONSES_STREAM_PLAN_KIND,
OPENAI_RESPONSES_STREAM_SUCCESS_REPORT_KIND, OPENAI_RESPONSES_SYNC_ERROR_REPORT_KIND,
OPENAI_RESPONSES_SYNC_FINALIZE_REPORT_KIND, OPENAI_RESPONSES_SYNC_PLAN_KIND,
OPENAI_RESPONSES_SYNC_SUCCESS_REPORT_KIND, OPENAI_VIDEO_CANCEL_SYNC_PLAN_KIND,
OPENAI_VIDEO_CONTENT_PLAN_KIND, OPENAI_VIDEO_CREATE_SYNC_FINALIZE_REPORT_KIND,
OPENAI_VIDEO_CREATE_SYNC_PLAN_KIND, OPENAI_VIDEO_DELETE_SYNC_PLAN_KIND,
OPENAI_VIDEO_REMIX_SYNC_PLAN_KIND,
};

View File

@@ -8,11 +8,12 @@ pub(crate) use crate::ai_pipeline::{
build_local_image_sync_plan_and_reports_for_kind,
build_local_openai_chat_stream_plan_and_reports_for_kind,
build_local_openai_chat_sync_plan_and_reports_for_kind,
build_local_openai_cli_stream_plan_and_reports_for_kind,
build_local_openai_cli_sync_plan_and_reports_for_kind,
build_local_openai_responses_stream_plan_and_reports_for_kind,
build_local_openai_responses_sync_plan_and_reports_for_kind,
build_local_same_format_stream_plan_and_reports, build_local_same_format_sync_plan_and_reports,
build_local_video_sync_plan_and_reports_for_kind, build_openai_cli_stream_plan_from_decision,
build_openai_cli_sync_plan_from_decision, build_passthrough_sync_plan_from_decision,
build_local_video_sync_plan_and_reports_for_kind,
build_openai_responses_stream_plan_from_decision,
build_openai_responses_sync_plan_from_decision, build_passthrough_sync_plan_from_decision,
build_standard_family_stream_plan_and_reports, build_standard_family_sync_plan_and_reports,
build_standard_stream_plan_from_decision, build_standard_sync_plan_from_decision,
maybe_build_stream_decision_payload, maybe_build_stream_plan_payload,
@@ -102,8 +103,10 @@ pub(crate) fn aggregate_openai_chat_stream_sync_response(body: &[u8]) -> Option<
aether_ai_pipeline::api::aggregate_openai_chat_stream_sync_response(body)
}
pub(crate) fn aggregate_openai_cli_stream_sync_response(body: &[u8]) -> Option<serde_json::Value> {
aether_ai_pipeline::api::aggregate_openai_cli_stream_sync_response(body)
pub(crate) fn aggregate_openai_responses_stream_sync_response(
body: &[u8],
) -> Option<serde_json::Value> {
aether_ai_pipeline::api::aggregate_openai_responses_stream_sync_response(body)
}
pub(crate) fn aggregate_claude_stream_sync_response(body: &[u8]) -> Option<serde_json::Value> {

View File

@@ -1,8 +1,8 @@
pub(crate) fn normalized_signature(api_format: &str) -> Option<&'static str> {
match api_format {
"openai:chat" => Some("openai:chat"),
"openai:cli" => Some("openai:cli"),
"openai:compact" => Some("openai:compact"),
"openai:responses" | "openai:cli" => Some("openai:responses"),
"openai:responses:compact" | "openai:compact" => Some("openai:responses:compact"),
"openai:image" => Some("openai:image"),
"openai:video" => Some("openai:video"),
_ => None,
@@ -12,8 +12,8 @@ pub(crate) fn normalized_signature(api_format: &str) -> Option<&'static str> {
pub(crate) fn local_path(api_format: &str) -> Option<&'static str> {
match api_format {
"openai" | "openai:chat" => Some("/v1/chat/completions"),
"openai:cli" => Some("/v1/responses"),
"openai:compact" => Some("/v1/responses/compact"),
"openai:responses" | "openai:cli" => Some("/v1/responses"),
"openai:responses:compact" | "openai:compact" => Some("/v1/responses/compact"),
"openai:image" => Some("/v1/images/generations"),
"openai:video" => Some("/v1/videos"),
_ => None,

View File

@@ -12,7 +12,7 @@ use crate::{AppState, GatewayError};
use super::super::GatewayControlDecision;
use super::credentials::{
build_auth_context_cache_key, contains_string, current_unix_secs, extract_request_credentials,
build_auth_context_cache_key, current_unix_secs, extract_request_credentials,
extract_trusted_admin_headers,
};
use super::gate::GatewayLocalAuthRejection;
@@ -631,7 +631,7 @@ async fn build_data_backed_auth_context(
})
} else if snapshot
.effective_allowed_api_formats()
.is_some_and(|allowed| !contains_string(allowed, auth_endpoint_signature))
.is_some_and(|allowed| !contains_api_format_or_alias(allowed, auth_endpoint_signature))
{
Some(GatewayLocalAuthRejection::ApiFormatNotAllowed {
api_format: auth_endpoint_signature.to_string(),
@@ -655,6 +655,22 @@ async fn build_data_backed_auth_context(
}
}
fn contains_api_format_or_alias(items: &[String], target: &str) -> bool {
items.iter().any(|item| api_format_matches(item, target))
}
fn normalize_api_format_alias(value: &str) -> String {
match value.trim().to_ascii_lowercase().as_str() {
"openai:cli" => "openai:responses".to_string(),
"openai:compact" => "openai:responses:compact".to_string(),
other => other.to_string(),
}
}
fn api_format_matches(left: &str, right: &str) -> bool {
normalize_api_format_alias(left) == normalize_api_format_alias(right)
}
async fn auth_snapshot_allows_requested_provider(
state: &AppState,
snapshot: &crate::data::auth::GatewayAuthApiKeySnapshot,

View File

@@ -23,12 +23,18 @@ pub(super) fn classify_ai_public_route(
Some(classified(
"ai_public",
"openai",
"compact",
"openai:compact",
"responses:compact",
"openai:responses:compact",
true,
))
} else {
Some(classified("ai_public", "openai", "cli", "openai:cli", true))
Some(classified(
"ai_public",
"openai",
"responses",
"openai:responses",
true,
))
}
} else if method == http::Method::POST
&& matches!(

View File

@@ -106,6 +106,8 @@ pub(crate) fn should_fallback_to_control_sync(
| "openai_video_remix_sync"
| "gemini_video_create_sync"
| "openai_chat_sync"
| "openai_responses_sync"
| "openai_responses_compact_sync"
| "openai_cli_sync"
| "openai_compact_sync"
| "claude_chat_sync"
@@ -151,6 +153,8 @@ pub(crate) fn resolve_core_sync_error_finalize_report_kind(
let report_kind = match plan_kind {
"openai_chat_sync" => "openai_chat_sync_finalize",
"openai_responses_sync" => "openai_responses_sync_finalize",
"openai_responses_compact_sync" => "openai_responses_compact_sync_finalize",
"openai_cli_sync" => "openai_cli_sync_finalize",
"openai_compact_sync" => "openai_compact_sync_finalize",
"claude_chat_sync" => "claude_chat_sync_finalize",
@@ -277,6 +281,8 @@ pub(crate) fn should_fallback_to_control_stream(
"openai_chat_stream"
| "claude_chat_stream"
| "gemini_chat_stream"
| "openai_responses_stream"
| "openai_responses_compact_stream"
| "openai_cli_stream"
| "openai_compact_stream"
| "claude_cli_stream"
@@ -296,6 +302,8 @@ pub(crate) fn resolve_core_stream_error_finalize_report_kind(
"openai_chat_stream" => "openai_chat_sync_finalize",
"claude_chat_stream" => "claude_chat_sync_finalize",
"gemini_chat_stream" => "gemini_chat_sync_finalize",
"openai_responses_stream" => "openai_responses_sync_finalize",
"openai_responses_compact_stream" => "openai_responses_compact_sync_finalize",
"openai_cli_stream" => "openai_cli_sync_finalize",
"openai_compact_stream" => "openai_compact_sync_finalize",
"claude_cli_stream" => "claude_cli_sync_finalize",
@@ -312,6 +320,8 @@ pub(crate) fn resolve_core_stream_direct_finalize_report_kind(plan_kind: &str) -
"openai_image_stream" => "openai_image_sync_finalize",
"claude_chat_stream" => "claude_chat_sync_finalize",
"gemini_chat_stream" => "gemini_chat_sync_finalize",
"openai_responses_stream" => "openai_responses_sync_finalize",
"openai_responses_compact_stream" => "openai_responses_compact_sync_finalize",
"openai_cli_stream" => "openai_cli_sync_finalize",
"openai_compact_stream" => "openai_compact_sync_finalize",
"claude_cli_stream" => "claude_cli_sync_finalize",

View File

@@ -281,6 +281,9 @@ fn merge_stream_terminal_summary(
current_summary.model = observed.model;
}
current_summary.observed_finish |= observed.observed_finish;
current_summary.unknown_event_count = current_summary
.unknown_event_count
.saturating_add(observed.unknown_event_count);
if current_summary.parser_error.is_none() {
current_summary.parser_error = observed.parser_error;
}
@@ -2270,12 +2273,14 @@ mod tests {
Some(ExecutionStreamTerminalSummary {
standardized_usage: Some(runtime_usage),
model: Some("gpt-5.5".to_string()),
unknown_event_count: 1,
..ExecutionStreamTerminalSummary::default()
}),
Some(ExecutionStreamTerminalSummary {
standardized_usage: Some(observed_usage),
response_id: Some("resp_123".to_string()),
observed_finish: true,
unknown_event_count: 2,
..ExecutionStreamTerminalSummary::default()
}),
)
@@ -2289,6 +2294,7 @@ mod tests {
assert_eq!(merged.model.as_deref(), Some("gpt-5.5"));
assert_eq!(merged.response_id.as_deref(), Some("resp_123"));
assert!(merged.observed_finish);
assert_eq!(merged.unknown_event_count, 3);
}
#[test]
@@ -2431,8 +2437,8 @@ mod tests {
"stream": true
})),
stream: true,
client_api_format: "openai:cli".into(),
provider_api_format: "openai:cli".into(),
client_api_format: "openai:responses".into(),
provider_api_format: "openai:responses".into(),
model_name: Some("gpt-5.4".into()),
proxy: None,
tls_profile: None,
@@ -2447,7 +2453,7 @@ mod tests {
Some("ai_public".to_string()),
Some("openai".to_string()),
Some("cli".to_string()),
Some("openai:cli".to_string()),
Some("openai:responses".to_string()),
)
.with_execution_runtime_candidate(true);
@@ -2456,11 +2462,11 @@ mod tests {
plan,
"trace-live-stream-first-data",
&decision,
"openai_cli_stream",
"openai_responses_stream",
None,
Some(json!({
"provider_api_format": "openai:cli",
"client_api_format": "openai:cli",
"provider_api_format": "openai:responses",
"client_api_format": "openai:responses",
})),
)
.await
@@ -2552,8 +2558,8 @@ mod tests {
"stream": true
})),
stream: true,
client_api_format: "openai:cli".into(),
provider_api_format: "openai:cli".into(),
client_api_format: "openai:responses".into(),
provider_api_format: "openai:responses".into(),
model_name: Some("gpt-5.4".into()),
proxy: None,
tls_profile: None,
@@ -2568,7 +2574,7 @@ mod tests {
Some("ai_public".to_string()),
Some("openai".to_string()),
Some("cli".to_string()),
Some("openai:cli".to_string()),
Some("openai:responses".to_string()),
)
.with_execution_runtime_candidate(true);
@@ -2577,11 +2583,11 @@ mod tests {
plan,
"trace-remote-runtime-sync-json-stream",
&decision,
"openai_cli_stream",
"openai_responses_stream",
None,
Some(json!({
"provider_api_format": "openai:cli",
"client_api_format": "openai:cli",
"provider_api_format": "openai:responses",
"client_api_format": "openai:responses",
})),
)
.await

View File

@@ -982,8 +982,8 @@ mod tests {
"stream": true
})),
stream: true,
client_api_format: "openai:cli".to_string(),
provider_api_format: "openai:cli".to_string(),
client_api_format: "openai:responses".to_string(),
provider_api_format: "openai:responses".to_string(),
model_name: Some("gpt-5.4".into()),
proxy: None,
tls_profile: None,

View File

@@ -5,9 +5,10 @@ use serde_json::json;
use crate::ai_pipeline_api::{
build_gemini_stream_plan_from_decision, build_gemini_sync_plan_from_decision,
build_openai_cli_stream_plan_from_decision, build_openai_cli_sync_plan_from_decision,
build_passthrough_sync_plan_from_decision, build_standard_stream_plan_from_decision,
build_standard_sync_plan_from_decision, GatewayControlSyncDecisionResponse,
build_openai_responses_stream_plan_from_decision,
build_openai_responses_sync_plan_from_decision, build_passthrough_sync_plan_from_decision,
build_standard_stream_plan_from_decision, build_standard_sync_plan_from_decision,
GatewayControlSyncDecisionResponse,
};
use crate::execution_runtime::submission::{
build_best_effort_local_core_error_body, resolve_core_error_background_report_kind,
@@ -102,10 +103,21 @@ fn resolve_core_error_background_report_kind_maps_all_core_finalize_kinds() {
("openai_chat_sync_finalize", Some("openai_chat_sync_error")),
("claude_chat_sync_finalize", Some("claude_chat_sync_error")),
("gemini_chat_sync_finalize", Some("gemini_chat_sync_error")),
("openai_cli_sync_finalize", Some("openai_cli_sync_error")),
(
"openai_responses_sync_finalize",
Some("openai_responses_sync_error"),
),
(
"openai_responses_compact_sync_finalize",
Some("openai_responses_compact_sync_error"),
),
(
"openai_cli_sync_finalize",
Some("openai_responses_sync_error"),
),
(
"openai_compact_sync_finalize",
Some("openai_compact_sync_error"),
Some("openai_responses_sync_error"),
),
("claude_cli_sync_finalize", Some("claude_cli_sync_error")),
("gemini_cli_sync_finalize", Some("gemini_cli_sync_error")),
@@ -138,10 +150,21 @@ fn resolve_core_success_background_report_kind_maps_all_core_finalize_kinds() {
"gemini_chat_sync_finalize",
Some("gemini_chat_sync_success"),
),
("openai_cli_sync_finalize", Some("openai_cli_sync_success")),
(
"openai_responses_sync_finalize",
Some("openai_responses_sync_success"),
),
(
"openai_responses_compact_sync_finalize",
Some("openai_responses_compact_sync_success"),
),
(
"openai_cli_sync_finalize",
Some("openai_responses_sync_success"),
),
(
"openai_compact_sync_finalize",
Some("openai_cli_sync_success"),
Some("openai_responses_sync_success"),
),
("claude_cli_sync_finalize", Some("claude_cli_sync_success")),
("gemini_cli_sync_finalize", Some("gemini_cli_sync_success")),
@@ -195,10 +218,10 @@ fn build_best_effort_local_core_error_body_converts_gemini_chat_error_to_openai_
}
#[test]
fn build_best_effort_local_core_error_body_converts_claude_cli_error_to_openai_cli() {
fn build_best_effort_local_core_error_body_converts_claude_cli_error_to_openai_responses() {
let payload = core_finalize_payload(
"openai_cli_sync_finalize",
"openai:cli",
"openai_responses_sync_finalize",
"openai:responses",
"claude:cli",
401,
json!({
@@ -300,10 +323,10 @@ fn build_best_effort_local_core_error_body_converts_sync_errors_across_standard_
}),
),
(
"gemini cli -> openai cli",
"gemini cli -> openai responses",
core_finalize_payload(
"openai_cli_sync_finalize",
"openai:cli",
"openai_responses_sync_finalize",
"openai:responses",
"gemini:cli",
429,
json!({
@@ -388,7 +411,7 @@ fn resolve_local_core_error_response_body_json_parses_body_base64_json_for_cross
let mut payload = core_finalize_payload(
"claude_cli_sync_finalize",
"claude:cli",
"openai:cli",
"openai:responses",
401,
json!({}),
);
@@ -419,7 +442,7 @@ fn resolve_local_core_error_response_body_json_builds_client_error_from_plain_te
let mut payload = core_finalize_payload(
"claude_cli_sync_finalize",
"claude:cli",
"openai:cli",
"openai:responses",
400,
json!({}),
);
@@ -515,18 +538,18 @@ fn generic_decision_builders_require_exact_provider_request() {
let parts = test_parts();
let body_json = json!({"messages":[{"role":"user","content":"hi"}]});
assert!(build_openai_cli_stream_plan_from_decision(
assert!(build_openai_responses_stream_plan_from_decision(
&parts,
&body_json,
missing_exact_provider_request_payload("openai_cli_stream"),
missing_exact_provider_request_payload("openai_responses_stream"),
false,
)
.expect("builder should not error")
.is_none());
assert!(build_openai_cli_stream_plan_from_decision(
assert!(build_openai_responses_stream_plan_from_decision(
&parts,
&body_json,
missing_exact_provider_request_payload("openai_compact_stream"),
missing_exact_provider_request_payload("openai_responses_compact_stream"),
true,
)
.expect("builder should not error")
@@ -546,10 +569,10 @@ fn generic_decision_builders_require_exact_provider_request() {
)
.expect("builder should not error")
.is_none());
assert!(build_openai_cli_sync_plan_from_decision(
assert!(build_openai_responses_sync_plan_from_decision(
&parts,
&body_json,
missing_exact_provider_request_payload("openai_cli_sync"),
missing_exact_provider_request_payload("openai_responses_sync"),
false,
)
.expect("builder should not error")
@@ -605,7 +628,7 @@ fn passthrough_sync_plan_uses_provider_request_method_override() {
}
#[test]
fn openai_cli_sync_plan_injects_auth_header_when_exact_headers_omit_it() {
fn openai_responses_sync_plan_injects_auth_header_when_exact_headers_omit_it() {
let request = Request::builder()
.method("POST")
.uri("/v1/responses")
@@ -613,10 +636,10 @@ fn openai_cli_sync_plan_injects_auth_header_when_exact_headers_omit_it() {
.expect("request");
let (parts, _) = request.into_parts();
let mut payload = missing_exact_provider_request_payload("openai_cli_sync");
let mut payload = missing_exact_provider_request_payload("openai_responses_sync");
payload.provider_name = Some("openai".to_string());
payload.provider_api_format = Some("openai:cli".to_string());
payload.client_api_format = Some("openai:cli".to_string());
payload.provider_api_format = Some("openai:responses".to_string());
payload.client_api_format = Some("openai:responses".to_string());
payload.model_name = Some("gpt-5".to_string());
payload.upstream_url = Some("https://chatgpt.com/backend-api/codex/responses".to_string());
payload.provider_request_headers =
@@ -626,7 +649,7 @@ fn openai_cli_sync_plan_injects_auth_header_when_exact_headers_omit_it() {
payload.provider_request_body = Some(json!({"model":"gpt-5"}));
let plan_and_report =
build_openai_cli_sync_plan_from_decision(&parts, &json!({}), payload, false)
build_openai_responses_sync_plan_from_decision(&parts, &json!({}), payload, false)
.expect("builder should not error")
.expect("plan should be built");
@@ -694,7 +717,7 @@ fn passthrough_sync_plan_uses_raw_body_bytes_when_decision_provides_base64_body(
}
#[test]
fn openai_cli_stream_plan_injects_auth_header_when_exact_headers_omit_it() {
fn openai_responses_stream_plan_injects_auth_header_when_exact_headers_omit_it() {
let request = Request::builder()
.method("POST")
.uri("/v1/responses")
@@ -702,10 +725,10 @@ fn openai_cli_stream_plan_injects_auth_header_when_exact_headers_omit_it() {
.expect("request");
let (parts, _) = request.into_parts();
let mut payload = missing_exact_provider_request_payload("openai_cli_stream");
let mut payload = missing_exact_provider_request_payload("openai_responses_stream");
payload.provider_name = Some("openai".to_string());
payload.provider_api_format = Some("openai:cli".to_string());
payload.client_api_format = Some("openai:cli".to_string());
payload.provider_api_format = Some("openai:responses".to_string());
payload.client_api_format = Some("openai:responses".to_string());
payload.model_name = Some("gpt-5".to_string());
payload.upstream_url = Some("https://chatgpt.com/backend-api/codex/responses".to_string());
payload.provider_request_headers =
@@ -715,7 +738,7 @@ fn openai_cli_stream_plan_injects_auth_header_when_exact_headers_omit_it() {
payload.provider_request_body = Some(json!({"model":"gpt-5","stream":true}));
let plan_and_report =
build_openai_cli_stream_plan_from_decision(&parts, &json!({}), payload, false)
build_openai_responses_stream_plan_from_decision(&parts, &json!({}), payload, false)
.expect("builder should not error")
.expect("plan should be built");
@@ -739,9 +762,9 @@ fn openai_cli_stream_plan_injects_auth_header_when_exact_headers_omit_it() {
#[test]
fn bypasses_execution_runtime_for_codex_backendapi_variant() {
let mut payload = missing_exact_provider_request_payload("openai_cli_stream");
payload.provider_api_format = Some("openai:cli".to_string());
payload.client_api_format = Some("openai:cli".to_string());
let mut payload = missing_exact_provider_request_payload("openai_responses_stream");
payload.provider_api_format = Some("openai:responses".to_string());
payload.client_api_format = Some("openai:responses".to_string());
payload.upstream_url = Some("https://chatgpt.com/backendapi/codex/responses".to_string());
assert!(should_bypass_execution_runtime_decision(&payload));
@@ -763,8 +786,8 @@ fn bypasses_execution_runtime_for_codex_plan_variant() {
content_encoding: None,
body: RequestBody::from_json(json!({"model":"gpt-5.4"})),
stream: true,
client_api_format: "openai:cli".to_string(),
provider_api_format: "openai:cli".to_string(),
client_api_format: "openai:responses".to_string(),
provider_api_format: "openai:responses".to_string(),
model_name: Some("gpt-5.4".to_string()),
proxy: None,
tls_profile: None,

View File

@@ -5,8 +5,8 @@ use crate::ai_pipeline_api::{
build_local_image_sync_plan_and_reports_for_kind,
build_local_openai_chat_stream_plan_and_reports_for_kind,
build_local_openai_chat_sync_plan_and_reports_for_kind,
build_local_openai_cli_stream_plan_and_reports_for_kind,
build_local_openai_cli_sync_plan_and_reports_for_kind,
build_local_openai_responses_stream_plan_and_reports_for_kind,
build_local_openai_responses_sync_plan_and_reports_for_kind,
build_local_same_format_stream_plan_and_reports, build_local_same_format_sync_plan_and_reports,
build_local_video_sync_plan_and_reports_for_kind,
build_standard_family_stream_plan_and_reports, build_standard_family_sync_plan_and_reports,
@@ -110,7 +110,7 @@ pub(crate) async fn maybe_execute_stream_via_local_decision(
Ok(outcome)
}
pub(crate) async fn maybe_execute_sync_via_local_openai_cli_decision(
pub(crate) async fn maybe_execute_sync_via_local_openai_responses_decision(
state: &AppState,
parts: &http::request::Parts,
trace_id: &str,
@@ -119,7 +119,7 @@ pub(crate) async fn maybe_execute_sync_via_local_openai_cli_decision(
plan_kind: &str,
) -> Result<LocalExecutionRequestOutcome, GatewayError> {
let plan_and_reports: Vec<LocalSyncPlanAndReport> =
build_local_openai_cli_sync_plan_and_reports_for_kind(
build_local_openai_responses_sync_plan_and_reports_for_kind(
state, parts, trace_id, decision, body_json, plan_kind,
)
.await?;
@@ -138,7 +138,7 @@ pub(crate) async fn maybe_execute_sync_via_local_openai_cli_decision(
.await
}
pub(crate) async fn maybe_execute_stream_via_local_openai_cli_decision(
pub(crate) async fn maybe_execute_stream_via_local_openai_responses_decision(
state: &AppState,
parts: &http::request::Parts,
trace_id: &str,
@@ -147,7 +147,7 @@ pub(crate) async fn maybe_execute_stream_via_local_openai_cli_decision(
plan_kind: &str,
) -> Result<LocalExecutionRequestOutcome, GatewayError> {
let plan_and_reports: Vec<LocalStreamPlanAndReport> =
build_local_openai_cli_stream_plan_and_reports_for_kind(
build_local_openai_responses_stream_plan_and_reports_for_kind(
state, parts, trace_id, decision, body_json, plan_kind,
)
.await?;

View File

@@ -1112,8 +1112,8 @@ mod tests {
candidate: skipped_candidate,
provider_name: Some("openai".to_string()),
key_name: Some("prod".to_string()),
client_api_format: Some("openai:cli".to_string()),
provider_api_format: Some("openai:cli".to_string()),
client_api_format: Some("openai:responses".to_string()),
provider_api_format: Some("openai:responses".to_string()),
global_model_name: Some("gpt-5".to_string()),
selected_provider_model_name: Some("gpt-5-upstream".to_string()),
endpoint_url: Some("https://api.openai.example/v1/responses".to_string()),

View File

@@ -24,7 +24,7 @@ pub(crate) fn should_bypass_execution_runtime_decision(
.to_ascii_lowercase();
if !matches!(
provider_api_format.as_str(),
"openai:cli" | "openai:compact"
"openai:responses" | "openai:cli" | "openai:compact" | "openai:responses:compact"
) {
return false;
}
@@ -42,7 +42,7 @@ pub(crate) fn should_bypass_execution_runtime_plan(plan: &ExecutionPlan) -> bool
let provider_api_format = plan.provider_api_format.trim().to_ascii_lowercase();
if !matches!(
provider_api_format.as_str(),
"openai:cli" | "openai:compact"
"openai:responses" | "openai:cli" | "openai:compact" | "openai:responses:compact"
) {
return false;
}

View File

@@ -15,7 +15,7 @@ use super::{
build_direct_plan_bypass_cache_key, execute_stream_plan_and_reports,
maybe_execute_stream_via_local_decision, maybe_execute_stream_via_local_gemini_files_decision,
maybe_execute_stream_via_local_image_decision,
maybe_execute_stream_via_local_openai_cli_decision,
maybe_execute_stream_via_local_openai_responses_decision,
maybe_execute_stream_via_local_same_format_provider_decision,
maybe_execute_stream_via_local_standard_decision, maybe_execute_stream_via_plan_fallback,
maybe_execute_stream_via_remote_decision, parse_local_request_body, should_skip_direct_plan,
@@ -90,7 +90,7 @@ pub(crate) async fn maybe_execute_via_stream_decision_path(
LocalExecutionRequestOutcome::NoPath => {}
}
match maybe_execute_stream_via_local_openai_cli_decision(
match maybe_execute_stream_via_local_openai_responses_decision(
state, parts, trace_id, decision, &body_json, plan_kind,
)
.await?

View File

@@ -16,7 +16,8 @@ use crate::{AppState, GatewayError, GatewayFallbackReason};
use super::{
build_direct_plan_bypass_cache_key, execute_sync_plan_and_reports,
maybe_execute_sync_via_local_decision, maybe_execute_sync_via_local_gemini_files_decision,
maybe_execute_sync_via_local_image_decision, maybe_execute_sync_via_local_openai_cli_decision,
maybe_execute_sync_via_local_image_decision,
maybe_execute_sync_via_local_openai_responses_decision,
maybe_execute_sync_via_local_same_format_provider_decision,
maybe_execute_sync_via_local_standard_decision, maybe_execute_sync_via_local_video_decision,
maybe_execute_sync_via_plan_fallback, maybe_execute_sync_via_remote_decision,
@@ -113,7 +114,7 @@ pub(crate) async fn maybe_execute_via_sync_decision_path(
LocalExecutionRequestOutcome::NoPath => {}
}
match maybe_execute_sync_via_local_openai_cli_decision(
match maybe_execute_sync_via_local_openai_responses_decision(
state, parts, trace_id, decision, &body_json, plan_kind,
)
.await?

View File

@@ -90,10 +90,10 @@ pub(super) async fn maybe_handle(
let endpoint = match normalized_provider_type.as_str() {
"codex" => endpoints.into_iter().find(|endpoint| {
endpoint.is_active
&& endpoint
.api_format
.trim()
.eq_ignore_ascii_case("openai:cli")
&& matches!(
endpoint.api_format.trim().to_ascii_lowercase().as_str(),
"openai:responses" | "openai:cli"
)
}),
"antigravity" => endpoints.into_iter().find(|endpoint| {
endpoint.is_active
@@ -122,7 +122,7 @@ pub(super) async fn maybe_handle(
let Some(endpoint) = endpoint else {
let detail = match normalized_provider_type.as_str() {
"codex" => "找不到有效的 openai:cli 端点",
"codex" => "找不到有效的 openai:responses 端点",
"antigravity" => "找不到有效的 gemini:chat/gemini:cli 端点",
"kiro" => "找不到有效的 Kiro 端点",
_ => "找不到有效端点",

View File

@@ -227,10 +227,10 @@ pub(super) async fn handle_admin_provider_oauth_complete_key(
.await?;
if let Some(endpoint) = endpoints.into_iter().find(|endpoint| {
endpoint.is_active
&& endpoint
.api_format
.trim()
.eq_ignore_ascii_case("openai:cli")
&& matches!(
endpoint.api_format.trim().to_ascii_lowercase().as_str(),
"openai:responses" | "openai:cli"
)
}) {
let refreshed_key = state
.read_provider_catalog_keys_by_ids(std::slice::from_ref(&key_id))

View File

@@ -93,8 +93,8 @@ pub(super) async fn execute_codex_quota_plan(
body_ref: None,
},
stream: false,
client_api_format: "openai:cli".to_string(),
provider_api_format: "openai:cli".to_string(),
client_api_format: "openai:responses".to_string(),
provider_api_format: "openai:responses".to_string(),
model_name: Some("codex-wham-usage".to_string()),
proxy,
tls_profile: state.resolve_transport_tls_profile(transport),

View File

@@ -19,10 +19,10 @@ pub(crate) fn provider_oauth_runtime_endpoint_for_provider(
.iter()
.find(|endpoint| {
endpoint.is_active
&& endpoint
.api_format
.trim()
.eq_ignore_ascii_case("openai:cli")
&& matches!(
endpoint.api_format.trim().to_ascii_lowercase().as_str(),
"openai:responses" | "openai:cli"
)
})
.cloned(),
"antigravity" => endpoints

View File

@@ -22,7 +22,6 @@ use crate::provider_transport::kiro::{
build_kiro_provider_request_body, supports_local_kiro_request_transport_with_network,
KiroProviderHeadersInput, KIRO_ENVELOPE_NAME,
};
use crate::provider_transport::url::build_openai_cli_url;
use crate::usage::GatewaySyncReportRequest;
use crate::{AppState, GatewayError};
use aether_admin::provider::pool as admin_provider_pool_pure;
@@ -282,10 +281,19 @@ fn provider_query_key_supports_endpoint(
return true;
}
let formats = provider_key_configured_api_formats(key);
let endpoint_api_format = provider_query_normalize_api_format_alias(endpoint_api_format);
formats.is_empty()
|| formats
.iter()
.any(|value| value.eq_ignore_ascii_case(endpoint_api_format))
.any(|value| provider_query_normalize_api_format_alias(value) == endpoint_api_format)
}
fn provider_query_normalize_api_format_alias(value: &str) -> String {
match value.trim().to_ascii_lowercase().as_str() {
"openai:cli" => "openai:responses".to_string(),
"openai:compact" => "openai:responses:compact".to_string(),
other => other.to_string(),
}
}
fn provider_query_transport_supports_standard_test_execution(
@@ -297,7 +305,7 @@ fn provider_query_transport_supports_standard_test_execution(
"openai:chat" => {
crate::provider_transport::policy::supports_local_openai_chat_transport(transport)
}
"openai:cli" => {
"openai:responses" | "openai:cli" => {
crate::provider_transport::policy::supports_local_standard_transport_with_network(
transport, api_format,
)
@@ -977,7 +985,7 @@ async fn provider_query_execute_standard_test_candidate(
}
provider_request_body
}
"openai:cli" => {
"openai:responses" | "openai:cli" => {
let Some(mut provider_request_body) =
crate::ai_pipeline::build_cross_format_openai_chat_request_body(
&request_body,
@@ -1001,14 +1009,14 @@ async fn provider_query_execute_standard_test_candidate(
format!("Provider request body rules rejected {provider_api_format}"),
));
}
crate::ai_pipeline::apply_codex_openai_cli_special_body_edits(
crate::ai_pipeline::apply_codex_openai_responses_special_body_edits(
&mut provider_request_body,
transport.provider.provider_type.as_str(),
provider_api_format,
transport.endpoint.body_rules.as_ref(),
Some(candidate.key.id.as_str()),
);
crate::ai_pipeline::apply_openai_compact_special_body_edits(
crate::ai_pipeline::apply_openai_responses_compact_special_body_edits(
&mut provider_request_body,
provider_api_format,
);
@@ -1030,12 +1038,12 @@ async fn provider_query_execute_standard_test_candidate(
None
};
let oauth_auth = match provider_api_format {
"openai:chat" | "openai:cli" | "claude:chat" | "claude:cli" | "gemini:chat"
| "gemini:cli" => state.resolve_local_oauth_header_auth(&transport).await?,
"openai:chat" | "openai:responses" | "openai:cli" | "claude:chat" | "claude:cli"
| "gemini:chat" | "gemini:cli" => state.resolve_local_oauth_header_auth(&transport).await?,
_ => None,
};
let auth = match provider_api_format {
"openai:chat" | "openai:cli" => {
"openai:chat" | "openai:responses" | "openai:cli" => {
crate::provider_transport::auth::resolve_local_openai_bearer_auth(&transport)
.or(oauth_auth)
}
@@ -1096,7 +1104,7 @@ async fn provider_query_execute_standard_test_candidate(
Some("application/json"),
)
}
"openai:cli" => {
"openai:responses" | "openai:cli" => {
crate::provider_transport::auth::build_complete_passthrough_headers_with_auth(
&parts.headers,
auth_header.as_deref().unwrap_or_default(),
@@ -1150,8 +1158,11 @@ async fn provider_query_execute_standard_test_candidate(
response_body: None,
});
}
if provider_api_format == "openai:cli" {
crate::ai_pipeline::apply_codex_openai_cli_special_headers(
if matches!(
provider_api_format.trim().to_ascii_lowercase().as_str(),
"openai:responses" | "openai:cli"
) {
crate::ai_pipeline::apply_codex_openai_responses_special_headers(
&mut request_headers,
&provider_request_body,
&parts.headers,
@@ -1254,7 +1265,10 @@ fn provider_query_prefers_chat_standard_test_api_format(api_format: &str) -> boo
}
fn provider_query_supports_cli_standard_test_api_format(api_format: &str) -> bool {
matches!(api_format, "openai:cli" | "claude:cli" | "gemini:cli")
matches!(
api_format,
"openai:responses" | "openai:cli" | "claude:cli" | "gemini:cli"
)
}
async fn build_admin_provider_query_kiro_failover_response(

View File

@@ -328,11 +328,8 @@ fn resolve_fixed_provider_endpoint_template<'a>(
}
template.endpoints.iter().find(|item| {
item.api_format
.eq_ignore_ascii_case(updated_endpoint.api_format.trim())
|| item
.api_format
.eq_ignore_ascii_case(existing_endpoint.api_format.trim())
api_format_matches(item.api_format, updated_endpoint.api_format.trim())
|| api_format_matches(item.api_format, existing_endpoint.api_format.trim())
})
}
@@ -349,6 +346,19 @@ fn endpoint_matches_fixed_provider_template(
.api_format
.trim()
.eq_ignore_ascii_case(endpoint_template.api_format)
|| api_format_matches(&endpoint.api_format, endpoint_template.api_format)
}
fn normalize_api_format_alias(value: &str) -> String {
match value.trim().to_ascii_lowercase().as_str() {
"openai:cli" => "openai:responses".to_string(),
"openai:compact" => "openai:responses:compact".to_string(),
other => other.to_string(),
}
}
fn api_format_matches(left: &str, right: &str) -> bool {
normalize_api_format_alias(left) == normalize_api_format_alias(right)
}
fn fixed_provider_endpoint_metadata(

View File

@@ -312,7 +312,16 @@ impl<'a> AdminAppState<'a> {
)?;
let provider_type = provider.provider_type.trim().to_ascii_lowercase();
if provider_type == "codex" && existing_endpoint.api_format == "openai:cli" {
if provider_type == "codex"
&& matches!(
existing_endpoint
.api_format
.trim()
.to_ascii_lowercase()
.as_str(),
"openai:responses" | "openai:cli"
)
{
let has_config_in_payload = fields.contains("config");
let config_payload = if has_config_in_payload {
updated
@@ -334,7 +343,7 @@ impl<'a> AdminAppState<'a> {
&& requested.is_some()
&& !admin_requested_force_stream(requested.expect("checked above"))
{
return Err("Codex OpenAI CLI 端点固定为强制流式,不允许修改".to_string());
return Err("Codex OpenAI Responses 端点固定为强制流式,不允许修改".to_string());
}
config.remove("upstreamStreamPolicy");
config.remove("upstream_stream");

View File

@@ -7,7 +7,7 @@ use super::internal::resolve_local_proxy_execution_path;
pub(crate) use super::public::matches_model_mapping_for_models;
use crate::ai_pipeline_api::{
aggregate_claude_stream_sync_response, aggregate_gemini_stream_sync_response,
aggregate_openai_chat_stream_sync_response, aggregate_openai_cli_stream_sync_response,
aggregate_openai_chat_stream_sync_response, aggregate_openai_responses_stream_sync_response,
maybe_bridge_standard_sync_json_to_stream,
};
use crate::api::response::{
@@ -66,7 +66,7 @@ const OPENAI_CHAT_LOCAL_EXECUTION_RUNTIME_MISS_DETAIL: &str =
"当前 OpenAI Chat Completions 请求无法在本地执行:没有匹配到可用的执行路径";
const OPENAI_RESPONSES_LOCAL_EXECUTION_RUNTIME_MISS_DETAIL: &str =
"当前 OpenAI Responses 请求无法在本地执行:没有匹配到可用的执行路径";
const OPENAI_COMPACT_LOCAL_EXECUTION_RUNTIME_MISS_DETAIL: &str =
const OPENAI_RESPONSES_COMPACT_LOCAL_EXECUTION_RUNTIME_MISS_DETAIL: &str =
"当前 OpenAI Responses Compact 请求无法在本地执行:没有匹配到可用的执行路径";
const OPENAI_VIDEO_LOCAL_EXECUTION_RUNTIME_MISS_DETAIL: &str =
"当前 OpenAI Video 请求无法在本地执行:没有匹配到可用的执行路径";
@@ -463,10 +463,12 @@ fn aggregate_sync_sse_response_for_client(
aggregate_openai_chat_stream_sync_response(body)
}
Some(value)
if value.eq_ignore_ascii_case("openai:cli")
if value.eq_ignore_ascii_case("openai:responses")
|| value.eq_ignore_ascii_case("openai:responses:compact")
|| value.eq_ignore_ascii_case("openai:cli")
|| value.eq_ignore_ascii_case("openai:compact") =>
{
aggregate_openai_cli_stream_sync_response(body)
aggregate_openai_responses_stream_sync_response(body)
}
Some(value)
if value.eq_ignore_ascii_case("claude:chat")
@@ -484,7 +486,7 @@ fn aggregate_sync_sse_response_for_client(
aggregate_openai_chat_stream_sync_response(body)
}
_ if public_path == "/v1/responses" || public_path == "/v1/responses/compact" => {
aggregate_openai_cli_stream_sync_response(body)
aggregate_openai_responses_stream_sync_response(body)
}
_ if public_path == "/v1/messages" => aggregate_claude_stream_sync_response(body),
_ if decision.route_family.as_deref() == Some("gemini")
@@ -531,15 +533,25 @@ fn resolve_affinity_forward_client_api_format(
.filter(|value| !value.is_empty());
match api_format {
Some(value) if value.eq_ignore_ascii_case("openai:chat") => Some("openai:chat"),
Some(value) if value.eq_ignore_ascii_case("openai:cli") => Some("openai:cli"),
Some(value) if value.eq_ignore_ascii_case("openai:compact") => Some("openai:compact"),
Some(value)
if value.eq_ignore_ascii_case("openai:responses")
|| value.eq_ignore_ascii_case("openai:cli") =>
{
Some("openai:responses")
}
Some(value)
if value.eq_ignore_ascii_case("openai:responses:compact")
|| value.eq_ignore_ascii_case("openai:compact") =>
{
Some("openai:responses:compact")
}
Some(value) if value.eq_ignore_ascii_case("claude:chat") => Some("claude:chat"),
Some(value) if value.eq_ignore_ascii_case("claude:cli") => Some("claude:cli"),
Some(value) if value.eq_ignore_ascii_case("gemini:chat") => Some("gemini:chat"),
Some(value) if value.eq_ignore_ascii_case("gemini:cli") => Some("gemini:cli"),
_ if public_path == "/v1/chat/completions" => Some("openai:chat"),
_ if public_path == "/v1/responses" => Some("openai:cli"),
_ if public_path == "/v1/responses/compact" => Some("openai:compact"),
_ if public_path == "/v1/responses" => Some("openai:responses"),
_ if public_path == "/v1/responses/compact" => Some("openai:responses:compact"),
_ if public_path == "/v1/messages" => Some("claude:chat"),
_ if decision.route_family.as_deref() == Some("gemini")
&& (public_path.contains(":generateContent")
@@ -1702,7 +1714,9 @@ fn local_execution_runtime_miss_route_detail(
match public_path {
"/v1/chat/completions" => Some(OPENAI_CHAT_LOCAL_EXECUTION_RUNTIME_MISS_DETAIL),
"/v1/responses" => Some(OPENAI_RESPONSES_LOCAL_EXECUTION_RUNTIME_MISS_DETAIL),
"/v1/responses/compact" => Some(OPENAI_COMPACT_LOCAL_EXECUTION_RUNTIME_MISS_DETAIL),
"/v1/responses/compact" => {
Some(OPENAI_RESPONSES_COMPACT_LOCAL_EXECUTION_RUNTIME_MISS_DETAIL)
}
"/v1/messages" => Some(CLAUDE_MESSAGES_LOCAL_EXECUTION_RUNTIME_MISS_DETAIL),
path if path.starts_with("/v1/videos") => {
Some(OPENAI_VIDEO_LOCAL_EXECUTION_RUNTIME_MISS_DETAIL)
@@ -1784,8 +1798,8 @@ mod tests {
"/v1/responses",
Some("ai_public".to_string()),
Some("openai".to_string()),
Some("cli".to_string()),
Some("openai:cli".to_string()),
Some("responses".to_string()),
Some("openai:responses".to_string()),
);
let diagnostic = LocalExecutionRuntimeMissDiagnostic {
reason: "all_candidates_skipped".to_string(),
@@ -1815,8 +1829,8 @@ mod tests {
"/v1/responses",
Some("ai_public".to_string()),
Some("openai".to_string()),
Some("cli".to_string()),
Some("openai:cli".to_string()),
Some("responses".to_string()),
Some("openai:responses".to_string()),
);
let diagnostic = LocalExecutionRuntimeMissDiagnostic {
reason: "all_candidates_skipped".to_string(),

View File

@@ -6,20 +6,27 @@ use aether_scheduler_core::matches_model_mapping;
use super::GatewayPublicRequestContext;
pub(crate) fn models_api_format(request_context: &GatewayPublicRequestContext) -> Option<&str> {
request_context
let signature = request_context
.control_decision
.as_ref()
.and_then(|decision| decision.auth_endpoint_signature.as_deref())
.filter(|signature| {
matches!(
*signature,
"openai:chat" | "openai:image" | "claude:chat" | "gemini:chat"
)
})
.map(str::trim)
.filter(|signature| !signature.is_empty())?;
match signature.to_ascii_lowercase().as_str() {
"openai:chat" => Some("openai:chat"),
"openai:responses" | "openai:cli" => Some("openai:responses"),
"openai:responses:compact" | "openai:compact" => Some("openai:responses:compact"),
"openai:image" => Some("openai:image"),
"claude:chat" => Some("claude:chat"),
"gemini:chat" => Some("gemini:chat"),
_ => None,
}
}
const MODELS_CROSS_FORMAT_QUERY_API_FORMATS: &[&str] = &[
"openai:chat",
"openai:responses",
"openai:responses:compact",
"openai:cli",
"openai:compact",
"openai:image",
@@ -31,7 +38,13 @@ const MODELS_CROSS_FORMAT_QUERY_API_FORMATS: &[&str] = &[
pub(super) fn models_query_api_formats(api_format: &str) -> &'static [&'static str] {
match api_format.trim().to_ascii_lowercase().as_str() {
"openai:chat" | "claude:chat" | "gemini:chat" => MODELS_CROSS_FORMAT_QUERY_API_FORMATS,
"openai:chat"
| "openai:responses"
| "openai:responses:compact"
| "openai:cli"
| "openai:compact"
| "claude:chat"
| "gemini:chat" => MODELS_CROSS_FORMAT_QUERY_API_FORMATS,
"openai:image" => &["openai:image"],
_ => &[],
}

View File

@@ -213,6 +213,8 @@ fn users_me_usage_api_format_defaults_to_non_stream(item: &StoredRequestUsageAud
api_format,
Some(value)
if value.eq_ignore_ascii_case("openai:chat")
|| value.eq_ignore_ascii_case("openai:responses")
|| value.eq_ignore_ascii_case("openai:responses:compact")
|| value.eq_ignore_ascii_case("openai:cli")
|| value.eq_ignore_ascii_case("openai:compact")
|| value.eq_ignore_ascii_case("openai:image")
@@ -1424,10 +1426,10 @@ mod tests {
}
#[test]
fn user_usage_stream_defaults_to_non_stream_for_openai_cli_request_body_without_flag() {
fn user_usage_stream_defaults_to_non_stream_for_openai_responses_request_body_without_flag() {
let item = StoredRequestUsageAudit {
is_stream: true,
api_format: Some("openai:cli".to_string()),
api_format: Some("openai:responses".to_string()),
request_body: Some(json!({
"model": "gpt-5.4",
"input": [{"role": "user", "content": "hi"}],

View File

@@ -601,7 +601,7 @@ mod tests {
));
assert!(!should_downgrade_access_log(
&Method::DELETE,
"/api/admin/monitoring/cache/affinity/provider/key/model/openai:cli"
"/api/admin/monitoring/cache/affinity/provider/key/model/openai:responses"
));
assert!(!should_downgrade_access_log(&Method::GET, "/v1/responses"));
}

View File

@@ -814,7 +814,12 @@ mod tests {
#[tokio::test]
async fn model_fetch_uses_preset_models_without_endpoint() {
let provider = sample_provider("provider-codex", "codex");
let key = sample_key("key-codex", "provider-codex", "api_key", &["openai:cli"]);
let key = sample_key(
"key-codex",
"provider-codex",
"api_key",
&["openai:responses"],
);
let state = TestState::new(vec![provider], vec![], vec![key], HashMap::new(), vec![]);
let summary = perform_model_fetch_once_with_state(&state)
@@ -944,7 +949,7 @@ mod tests {
assert_eq!(updated.allowed_models, Some(json!(["gpt-old"])));
assert_eq!(
updated.last_models_fetch_error.as_deref(),
Some("No supported endpoint for Rust models fetch")
Some("Provider transport snapshot unavailable")
);
}
}

View File

@@ -681,8 +681,8 @@ mod tests {
content_encoding: None,
body: RequestBody::from_json(json!({"model":"gpt-5.4"})),
stream: false,
client_api_format: "openai:cli".to_string(),
provider_api_format: "openai:cli".to_string(),
client_api_format: "openai:responses".to_string(),
provider_api_format: "openai:responses".to_string(),
model_name: Some("gpt-5.4".to_string()),
proxy: None,
tls_profile: None,
@@ -715,7 +715,7 @@ mod tests {
StoredProviderCatalogEndpoint::new(
"endpoint-codex-cli-local-1".to_string(),
"provider-codex-cli-local-1".to_string(),
"openai:cli".to_string(),
"openai:responses".to_string(),
Some("openai".to_string()),
Some("cli".to_string()),
true,
@@ -750,12 +750,12 @@ mod tests {
)
.expect("key should build")
.with_transport_fields(
Some(serde_json::json!(["openai:cli"])),
Some(serde_json::json!(["openai:responses"])),
encrypt_python_fernet_plaintext(DEVELOPMENT_ENCRYPTION_KEY, "__placeholder__")
.expect("placeholder api key should encrypt"),
Some(encrypted_auth_config),
None,
Some(serde_json::json!({"openai:cli": 1})),
Some(serde_json::json!({"openai:responses": 1})),
None,
None,
None,

View File

@@ -306,15 +306,15 @@ mod tests {
#[test]
fn deduplicates_active_provider_api_formats() {
let endpoints = vec![
sample_endpoint("openai:cli", true),
sample_endpoint("openai:responses", true),
sample_endpoint("openai:image", true),
sample_endpoint("openai:cli", true),
sample_endpoint("openai:compact", false),
sample_endpoint("openai:responses", true),
sample_endpoint("openai:responses:compact", false),
];
assert_eq!(
provider_active_api_formats(&endpoints),
vec!["openai:cli".to_string(), "openai:image".to_string()]
vec!["openai:responses".to_string(), "openai:image".to_string()]
);
}
@@ -322,14 +322,14 @@ mod tests {
fn fixed_oauth_key_with_null_formats_inherits_provider_formats() {
let key = sample_key("oauth");
let endpoints = vec![
sample_endpoint("openai:cli", true),
sample_endpoint("openai:responses", true),
sample_endpoint("openai:image", true),
];
assert!(provider_key_inherits_provider_api_formats(&key, "codex"));
assert_eq!(
provider_key_effective_api_formats(&key, "codex", &endpoints),
vec!["openai:cli".to_string(), "openai:image".to_string()]
vec!["openai:responses".to_string(), "openai:image".to_string()]
);
}
@@ -338,34 +338,34 @@ mod tests {
let mut key = sample_key("oauth");
key.api_formats = Some(json!(["openai:compact"]));
let endpoints = vec![
sample_endpoint("openai:cli", true),
sample_endpoint("openai:responses", true),
sample_endpoint("openai:image", true),
];
assert!(provider_key_inherits_provider_api_formats(&key, "codex"));
assert_eq!(
provider_key_effective_api_formats(&key, "codex", &endpoints),
vec!["openai:cli".to_string(), "openai:image".to_string()]
vec!["openai:responses".to_string(), "openai:image".to_string()]
);
}
#[test]
fn explicit_formats_do_not_inherit_for_non_fixed_key() {
let mut key = sample_key("oauth");
key.api_formats = Some(json!(["openai:compact"]));
key.api_formats = Some(json!(["openai:responses:compact"]));
let endpoints = vec![
sample_endpoint("openai:cli", true),
sample_endpoint("openai:responses", true),
sample_endpoint("openai:image", true),
];
assert!(!provider_key_inherits_provider_api_formats(&key, "openai"));
assert_eq!(
provider_key_configured_api_formats(&key),
vec!["openai:compact".to_string()]
vec!["openai:responses:compact".to_string()]
);
assert_eq!(
provider_key_effective_api_formats(&key, "openai", &endpoints),
vec!["openai:compact".to_string()]
vec!["openai:responses:compact".to_string()]
);
}
}

View File

@@ -19,5 +19,44 @@ pub(crate) fn read_cached_scheduler_affinity_target(
api_format,
global_model_name,
)?;
state.read_cached_scheduler_affinity_target(&cache_key, SCHEDULER_AFFINITY_TTL)
state
.read_cached_scheduler_affinity_target(&cache_key, SCHEDULER_AFFINITY_TTL)
.or_else(|| {
openai_responses_affinity_alias(api_format)
.and_then(|alias| {
legacy_raw_scheduler_affinity_cache_key(api_key_id, alias, global_model_name)
})
.and_then(|alias_cache_key| {
state.read_cached_scheduler_affinity_target(
&alias_cache_key,
SCHEDULER_AFFINITY_TTL,
)
})
})
}
fn openai_responses_affinity_alias(api_format: &str) -> Option<&'static str> {
match api_format.trim().to_ascii_lowercase().as_str() {
"openai:responses" => Some("openai:cli"),
"openai:responses:compact" => Some("openai:compact"),
"openai:cli" => Some("openai:responses"),
"openai:compact" => Some("openai:responses:compact"),
_ => None,
}
}
fn legacy_raw_scheduler_affinity_cache_key(
api_key_id: &str,
api_format: &str,
global_model_name: &str,
) -> Option<String> {
let api_key_id = api_key_id.trim();
let api_format = api_format.trim().to_ascii_lowercase();
let global_model_name = global_model_name.trim();
if api_key_id.is_empty() || api_format.is_empty() || global_model_name.is_empty() {
return None;
}
Some(format!(
"scheduler_affinity:{api_key_id}:{api_format}:{global_model_name}"
))
}

View File

@@ -534,8 +534,8 @@ async fn gateway_locally_denies_openai_responses_after_execution_runtime_miss_wi
assert_ai_route_locally_denied_after_execution_runtime_miss(
"/v1/responses",
"openai",
"cli",
"openai:cli",
"responses",
"openai:responses",
"{\"model\":\"gpt-5\",\"input\":\"hello\"}",
"请求缺少有效的用户或 API Key 认证上下文,无法选择上游提供商",
)
@@ -562,8 +562,8 @@ async fn gateway_locally_denies_openai_responses_stream_after_execution_runtime_
assert_ai_route_locally_denied_after_execution_runtime_miss(
"/v1/responses",
"openai",
"cli",
"openai:cli",
"responses",
"openai:responses",
"{\"model\":\"gpt-5\",\"input\":\"hello\",\"stream\":true}",
"请求缺少有效的用户或 API Key 认证上下文,无法选择上游提供商",
)
@@ -590,8 +590,8 @@ async fn gateway_locally_denies_openai_compact_after_execution_runtime_miss_with
assert_ai_route_locally_denied_after_execution_runtime_miss(
"/v1/responses/compact",
"openai",
"compact",
"openai:compact",
"responses:compact",
"openai:responses:compact",
"{\"model\":\"gpt-5\",\"input\":\"hello\"}",
"请求缺少有效的用户或 API Key 认证上下文,无法选择上游提供商",
)
@@ -604,8 +604,8 @@ async fn gateway_locally_denies_openai_compact_stream_after_execution_runtime_mi
assert_ai_route_locally_denied_after_execution_runtime_miss(
"/v1/responses/compact",
"openai",
"compact",
"openai:compact",
"responses:compact",
"openai:responses:compact",
"{\"model\":\"gpt-5\",\"input\":\"hello\",\"stream\":true}",
"请求缺少有效的用户或 API Key 认证上下文,无法选择上游提供商",
)

View File

@@ -442,6 +442,7 @@ async fn gateway_executes_openai_cli_sync_via_local_decision_gate_with_local_syn
.await
.expect("request should succeed");
eprintln!("codex oauth response status: {}", response.status());
assert_eq!(response.status(), StatusCode::OK);
assert_eq!(
response

View File

@@ -479,7 +479,7 @@ fn ai_pipeline_candidate_preparation_owns_shared_auth_and_mapped_model_resolutio
for path in [
"apps/aether-gateway/src/ai_pipeline/planner/standard/openai/chat/decision/request.rs",
"apps/aether-gateway/src/ai_pipeline/planner/standard/openai/cli/decision/request.rs",
"apps/aether-gateway/src/ai_pipeline/planner/standard/openai/responses/decision/request.rs",
"apps/aether-gateway/src/ai_pipeline/planner/specialized/image/request.rs",
"apps/aether-gateway/src/ai_pipeline/planner/standard/family/request.rs",
] {
@@ -555,7 +555,7 @@ fn ai_pipeline_candidate_materialization_owns_affinity_and_candidate_runtime_per
"apps/aether-gateway/src/ai_pipeline/planner/standard/family/candidates.rs",
"apps/aether-gateway/src/ai_pipeline/planner/passthrough/provider/family/candidates.rs",
"apps/aether-gateway/src/ai_pipeline/planner/standard/openai/chat/decision/support.rs",
"apps/aether-gateway/src/ai_pipeline/planner/standard/openai/cli/decision/support.rs",
"apps/aether-gateway/src/ai_pipeline/planner/standard/openai/responses/decision/support.rs",
"apps/aether-gateway/src/ai_pipeline/planner/specialized/video/support.rs",
"apps/aether-gateway/src/ai_pipeline/planner/specialized/files/support.rs",
] {
@@ -586,7 +586,7 @@ fn ai_pipeline_candidate_materialization_owns_affinity_and_candidate_runtime_per
"apps/aether-gateway/src/ai_pipeline/planner/standard/family/payload.rs",
"apps/aether-gateway/src/ai_pipeline/planner/passthrough/provider/family/payload.rs",
"apps/aether-gateway/src/ai_pipeline/planner/standard/openai/chat/decision/support.rs",
"apps/aether-gateway/src/ai_pipeline/planner/standard/openai/cli/decision/support.rs",
"apps/aether-gateway/src/ai_pipeline/planner/standard/openai/responses/decision/support.rs",
"apps/aether-gateway/src/ai_pipeline/planner/specialized/video/support.rs",
"apps/aether-gateway/src/ai_pipeline/planner/specialized/files/support.rs",
] {
@@ -611,8 +611,8 @@ fn ai_pipeline_candidate_materialization_owns_affinity_and_candidate_runtime_per
"LocalExecutionCandidateAttempt as LocalOpenAiChatCandidateAttempt",
),
(
"apps/aether-gateway/src/ai_pipeline/planner/standard/openai/cli/decision/support.rs",
"LocalExecutionCandidateAttempt as LocalOpenAiCliCandidateAttempt",
"apps/aether-gateway/src/ai_pipeline/planner/standard/openai/responses/decision/support.rs",
"LocalExecutionCandidateAttempt as LocalOpenAiResponsesCandidateAttempt",
),
(
"apps/aether-gateway/src/ai_pipeline/planner/specialized/video/support.rs",
@@ -649,7 +649,7 @@ fn ai_pipeline_materialization_policy_owns_local_candidate_persistence_modes() {
"LocalCandidatePersistencePolicyKind::StandardDecision",
"LocalCandidatePersistencePolicyKind::SameFormatProviderDecision",
"LocalCandidatePersistencePolicyKind::OpenAiChatDecision",
"LocalCandidatePersistencePolicyKind::OpenAiCliDecision",
"LocalCandidatePersistencePolicyKind::OpenAiResponsesDecision",
"LocalCandidatePersistencePolicyKind::GeminiFilesDecision",
"LocalCandidatePersistencePolicyKind::VideoDecision",
] {
@@ -663,7 +663,7 @@ fn ai_pipeline_materialization_policy_owns_local_candidate_persistence_modes() {
"apps/aether-gateway/src/ai_pipeline/planner/standard/family/candidates.rs",
"apps/aether-gateway/src/ai_pipeline/planner/passthrough/provider/family/candidates.rs",
"apps/aether-gateway/src/ai_pipeline/planner/standard/openai/chat/decision/support.rs",
"apps/aether-gateway/src/ai_pipeline/planner/standard/openai/cli/decision/support.rs",
"apps/aether-gateway/src/ai_pipeline/planner/standard/openai/responses/decision/support.rs",
"apps/aether-gateway/src/ai_pipeline/planner/specialized/video/support.rs",
"apps/aether-gateway/src/ai_pipeline/planner/specialized/files/support.rs",
"apps/aether-gateway/src/ai_pipeline/planner/standard/family/payload.rs",
@@ -716,7 +716,7 @@ fn ai_pipeline_candidate_metadata_owns_local_execution_candidate_extra_data_shap
for path in [
"apps/aether-gateway/src/ai_pipeline/planner/standard/family/candidates.rs",
"apps/aether-gateway/src/ai_pipeline/planner/standard/openai/chat/decision/support.rs",
"apps/aether-gateway/src/ai_pipeline/planner/standard/openai/cli/decision/support.rs",
"apps/aether-gateway/src/ai_pipeline/planner/standard/openai/responses/decision/support.rs",
"apps/aether-gateway/src/ai_pipeline/planner/passthrough/provider/family/candidates.rs",
"apps/aether-gateway/src/ai_pipeline/planner/specialized/files/support.rs",
"apps/aether-gateway/src/ai_pipeline/planner/specialized/video/support.rs",
@@ -1116,7 +1116,7 @@ fn ai_pipeline_payload_metadata_owns_local_execution_decision_response_shape() {
"apps/aether-gateway/src/ai_pipeline/planner/standard/family/payload.rs",
"apps/aether-gateway/src/ai_pipeline/planner/passthrough/provider/family/payload.rs",
"apps/aether-gateway/src/ai_pipeline/planner/standard/openai/chat/decision/payload.rs",
"apps/aether-gateway/src/ai_pipeline/planner/standard/openai/cli/decision/payload.rs",
"apps/aether-gateway/src/ai_pipeline/planner/standard/openai/responses/decision/payload.rs",
"apps/aether-gateway/src/ai_pipeline/planner/specialized/video/decision.rs",
"apps/aether-gateway/src/ai_pipeline/planner/specialized/files/decision.rs",
] {
@@ -1156,7 +1156,7 @@ fn ai_pipeline_report_context_owns_local_execution_context_shape() {
"apps/aether-gateway/src/ai_pipeline/planner/standard/family/payload.rs",
"apps/aether-gateway/src/ai_pipeline/planner/passthrough/provider/family/payload.rs",
"apps/aether-gateway/src/ai_pipeline/planner/standard/openai/chat/decision/payload.rs",
"apps/aether-gateway/src/ai_pipeline/planner/standard/openai/cli/decision/payload.rs",
"apps/aether-gateway/src/ai_pipeline/planner/standard/openai/responses/decision/payload.rs",
"apps/aether-gateway/src/ai_pipeline/planner/specialized/video/decision.rs",
"apps/aether-gateway/src/ai_pipeline/planner/specialized/files/decision.rs",
] {
@@ -1188,13 +1188,13 @@ fn ai_pipeline_standard_attempts_consume_eligible_local_candidates_without_trans
"openai chat attempts should reuse shared LocalExecutionCandidateAttempt"
);
let openai_cli_support = read_workspace_file(
"apps/aether-gateway/src/ai_pipeline/planner/standard/openai/cli/decision/support.rs",
let openai_responses_support = read_workspace_file(
"apps/aether-gateway/src/ai_pipeline/planner/standard/openai/responses/decision/support.rs",
);
assert!(
openai_cli_support
.contains("LocalExecutionCandidateAttempt as LocalOpenAiCliCandidateAttempt"),
"openai cli attempts should reuse shared LocalExecutionCandidateAttempt"
openai_responses_support
.contains("LocalExecutionCandidateAttempt as LocalOpenAiResponsesCandidateAttempt"),
"openai responses attempts should reuse shared LocalExecutionCandidateAttempt"
);
let standard_family_mod =
@@ -1208,7 +1208,7 @@ fn ai_pipeline_standard_attempts_consume_eligible_local_candidates_without_trans
for path in [
"apps/aether-gateway/src/ai_pipeline/planner/standard/openai/chat/decision.rs",
"apps/aether-gateway/src/ai_pipeline/planner/standard/openai/chat/decision/request.rs",
"apps/aether-gateway/src/ai_pipeline/planner/standard/openai/cli/decision/request.rs",
"apps/aether-gateway/src/ai_pipeline/planner/standard/openai/responses/decision/request.rs",
"apps/aether-gateway/src/ai_pipeline/planner/standard/family/request.rs",
] {
let source = read_workspace_file(path);
@@ -1266,7 +1266,7 @@ fn ai_pipeline_candidate_sources_share_cross_format_auth_filter_helper() {
for path in [
"apps/aether-gateway/src/ai_pipeline/planner/standard/openai/chat/plans/candidates.rs",
"apps/aether-gateway/src/ai_pipeline/planner/standard/openai/cli/decision/support.rs",
"apps/aether-gateway/src/ai_pipeline/planner/standard/openai/responses/decision/support.rs",
"apps/aether-gateway/src/ai_pipeline/planner/standard/family/candidates.rs",
] {
let source = read_workspace_file(path);
@@ -1294,7 +1294,7 @@ fn ai_pipeline_spec_metadata_owns_family_requested_model_and_plan_builder_routin
"pub(crate) fn requested_model_family_for_video_create(",
"pub(crate) fn local_standard_spec_metadata(",
"pub(crate) fn local_same_format_provider_spec_metadata(",
"pub(crate) fn local_openai_cli_spec_metadata(",
"pub(crate) fn local_openai_responses_spec_metadata(",
"pub(crate) fn local_gemini_files_spec_metadata(",
"pub(crate) fn local_video_create_spec_metadata(",
"pub(crate) fn build_sync_plan_from_requested_model_family(",
@@ -1364,20 +1364,20 @@ fn ai_pipeline_spec_metadata_owns_family_requested_model_and_plan_builder_routin
"local_gemini_files_spec_metadata(",
),
(
"apps/aether-gateway/src/ai_pipeline/planner/standard/openai/cli/plans.rs",
"local_openai_cli_spec_metadata(",
"apps/aether-gateway/src/ai_pipeline/planner/standard/openai/responses/plans.rs",
"local_openai_responses_spec_metadata(",
),
(
"apps/aether-gateway/src/ai_pipeline/planner/standard/openai/cli/decision/support.rs",
"local_openai_cli_spec_metadata(",
"apps/aether-gateway/src/ai_pipeline/planner/standard/openai/responses/decision/support.rs",
"local_openai_responses_spec_metadata(",
),
(
"apps/aether-gateway/src/ai_pipeline/planner/standard/openai/cli/decision/request.rs",
"local_openai_cli_spec_metadata(",
"apps/aether-gateway/src/ai_pipeline/planner/standard/openai/responses/decision/request.rs",
"local_openai_responses_spec_metadata(",
),
(
"apps/aether-gateway/src/ai_pipeline/planner/standard/openai/cli/decision/payload.rs",
"local_openai_cli_spec_metadata(",
"apps/aether-gateway/src/ai_pipeline/planner/standard/openai/responses/decision/payload.rs",
"local_openai_responses_spec_metadata(",
),
(
"apps/aether-gateway/src/ai_pipeline/planner/standard/family/build.rs",
@@ -1487,7 +1487,7 @@ fn ai_pipeline_decision_inputs_share_authenticated_input_helper() {
for path in [
"apps/aether-gateway/src/ai_pipeline/planner/standard/family/candidates.rs",
"apps/aether-gateway/src/ai_pipeline/planner/passthrough/provider/family/candidates.rs",
"apps/aether-gateway/src/ai_pipeline/planner/standard/openai/cli/decision/support.rs",
"apps/aether-gateway/src/ai_pipeline/planner/standard/openai/responses/decision/support.rs",
"apps/aether-gateway/src/ai_pipeline/planner/standard/openai/chat/plans/resolve.rs",
"apps/aether-gateway/src/ai_pipeline/planner/specialized/video/support.rs",
"apps/aether-gateway/src/ai_pipeline/planner/specialized/files/support.rs",
@@ -1497,7 +1497,7 @@ fn ai_pipeline_decision_inputs_share_authenticated_input_helper() {
source.contains("resolve_local_authenticated_decision_input("),
"{path} should use the shared authenticated decision input helper"
);
if path.ends_with("/standard/openai/cli/decision/support.rs")
if path.ends_with("/standard/openai/responses/decision/support.rs")
|| path.ends_with("/standard/openai/chat/plans/resolve.rs")
{
assert!(
@@ -1537,8 +1537,8 @@ fn ai_pipeline_decision_inputs_share_authenticated_input_helper() {
"LocalRequestedModelDecisionInput as LocalOpenAiChatDecisionInput",
),
(
"apps/aether-gateway/src/ai_pipeline/planner/standard/openai/cli/decision/support.rs",
"LocalRequestedModelDecisionInput as LocalOpenAiCliDecisionInput",
"apps/aether-gateway/src/ai_pipeline/planner/standard/openai/responses/decision/support.rs",
"LocalRequestedModelDecisionInput as LocalOpenAiResponsesDecisionInput",
),
(
"apps/aether-gateway/src/ai_pipeline/planner/specialized/video/support.rs",
@@ -1570,7 +1570,7 @@ fn ai_pipeline_decision_inputs_share_authenticated_input_helper() {
"build_local_requested_model_decision_input(",
),
(
"apps/aether-gateway/src/ai_pipeline/planner/standard/openai/cli/decision/support.rs",
"apps/aether-gateway/src/ai_pipeline/planner/standard/openai/responses/decision/support.rs",
"build_local_requested_model_decision_input(",
),
(
@@ -1596,7 +1596,7 @@ fn ai_pipeline_leaf_planner_owners_route_contract_specs_through_gateway_seams()
"apps/aether-gateway/src/ai_pipeline/planner/specialized/files/support.rs",
"apps/aether-gateway/src/ai_pipeline/planner/specialized/video/support.rs",
"apps/aether-gateway/src/ai_pipeline/planner/standard/openai/chat/decision/support.rs",
"apps/aether-gateway/src/ai_pipeline/planner/standard/openai/cli/decision/support.rs",
"apps/aether-gateway/src/ai_pipeline/planner/standard/openai/responses/decision/support.rs",
] {
let source = read_workspace_file(path);
assert!(
@@ -1900,15 +1900,15 @@ fn ai_pipeline_finalize_standard_sync_response_converters_are_owned_by_pipeline_
for (candidate_paths, symbol) in [
(
vec!["apps/aether-gateway/src/ai_pipeline/finalize/standard/mod.rs"],
"convert_openai_cli_response_to_openai_chat",
"convert_openai_responses_response_to_openai_chat",
),
(
vec!["apps/aether-gateway/src/ai_pipeline/finalize/standard/mod.rs"],
"build_openai_cli_response",
"build_openai_responses_response",
),
(
vec!["apps/aether-gateway/src/ai_pipeline/finalize/standard/mod.rs"],
"convert_openai_chat_response_to_openai_cli",
"convert_openai_chat_response_to_openai_responses",
),
(
vec!["apps/aether-gateway/src/ai_pipeline/finalize/standard/mod.rs"],
@@ -1920,7 +1920,7 @@ fn ai_pipeline_finalize_standard_sync_response_converters_are_owned_by_pipeline_
),
(
vec!["apps/aether-gateway/src/ai_pipeline/finalize/standard/mod.rs"],
"convert_claude_cli_response_to_openai_cli",
"convert_claude_response_to_openai_responses",
),
(
vec!["apps/aether-gateway/src/ai_pipeline/finalize/standard/mod.rs"],
@@ -1932,7 +1932,7 @@ fn ai_pipeline_finalize_standard_sync_response_converters_are_owned_by_pipeline_
),
(
vec!["apps/aether-gateway/src/ai_pipeline/finalize/standard/mod.rs"],
"convert_gemini_cli_response_to_openai_cli",
"convert_gemini_response_to_openai_responses",
),
] {
let sources = candidate_paths
@@ -2032,14 +2032,14 @@ fn ai_pipeline_finalize_standard_sync_products_are_owned_by_pipeline_crate() {
for expected in [
"pub fn maybe_build_standard_cross_format_sync_product_from_normalized_payload(",
"pub fn maybe_build_standard_same_format_sync_body_from_normalized_payload(",
"pub fn maybe_build_openai_cli_same_family_sync_body_from_normalized_payload(",
"pub fn maybe_build_openai_responses_same_family_sync_body_from_normalized_payload(",
"pub fn maybe_build_openai_chat_cross_format_sync_product_from_normalized_payload(",
"pub fn maybe_build_openai_cli_cross_format_sync_product_from_normalized_payload(",
"pub fn maybe_build_openai_responses_cross_format_sync_product_from_normalized_payload(",
"pub fn maybe_build_standard_sync_finalize_product_from_normalized_payload(",
"pub fn aggregate_standard_chat_stream_sync_response(",
"pub fn aggregate_standard_cli_stream_sync_response(",
"pub fn aggregate_openai_chat_stream_sync_response(",
"pub fn aggregate_openai_cli_stream_sync_response(",
"pub fn aggregate_openai_responses_stream_sync_response(",
"pub fn aggregate_claude_stream_sync_response(",
"pub fn aggregate_gemini_stream_sync_response(",
"pub fn convert_standard_chat_response(",
@@ -2315,7 +2315,7 @@ fn ai_pipeline_planner_standard_normalize_is_owned_by_pipeline_crate() {
"apps/aether-gateway/src/ai_pipeline/planner/standard/normalize/chat.rs",
);
let gateway_normalize_cli = read_workspace_file(
"apps/aether-gateway/src/ai_pipeline/planner/standard/normalize/cli.rs",
"apps/aether-gateway/src/ai_pipeline/planner/standard/normalize/responses.rs",
);
assert!(
gateway_normalize_chat.contains("crate::ai_pipeline::")
@@ -2325,7 +2325,7 @@ fn ai_pipeline_planner_standard_normalize_is_owned_by_pipeline_crate() {
for forbidden in [
"serde_json::Map::from_iter",
"normalize_openai_cli_request_to_openai_chat_request",
"normalize_openai_responses_request_to_openai_chat_request",
"parse_openai_tool_result_content",
] {
assert!(
@@ -2648,41 +2648,41 @@ fn ai_pipeline_specialized_video_specs_are_owned_by_pipeline_crate() {
}
#[test]
fn ai_pipeline_openai_cli_specs_are_owned_by_pipeline_crate() {
fn ai_pipeline_openai_responses_specs_are_owned_by_pipeline_crate() {
assert!(
workspace_file_exists("crates/aether-ai-pipeline/src/planner/standard/openai_cli.rs"),
"planner/standard/openai_cli pure spec owner should live in aether-ai-pipeline"
workspace_file_exists("crates/aether-ai-pipeline/src/planner/standard/openai_responses.rs"),
"planner/standard/openai_responses pure spec owner should live in aether-ai-pipeline"
);
let decision = read_workspace_file(
"apps/aether-gateway/src/ai_pipeline/planner/standard/openai/cli/decision.rs",
"apps/aether-gateway/src/ai_pipeline/planner/standard/openai/responses/decision.rs",
);
assert!(
decision.contains("pub(super) use crate::ai_pipeline::LocalOpenAiCliSpec;"),
"gateway planner/standard/openai/cli/decision.rs should re-export pure openai-cli spec type through the ai_pipeline root seam"
decision.contains("pub(super) use crate::ai_pipeline::LocalOpenAiResponsesSpec;"),
"gateway planner/standard/openai/responses/decision.rs should re-export pure openai-responses spec type through the ai_pipeline root seam"
);
assert!(
!decision.contains("pub(super) struct LocalOpenAiCliSpec"),
"gateway planner/standard/openai/cli/decision.rs should not own LocalOpenAiCliSpec"
!decision.contains("pub(super) struct LocalOpenAiResponsesSpec"),
"gateway planner/standard/openai/responses/decision.rs should not own LocalOpenAiResponsesSpec"
);
let plans = read_workspace_file(
"apps/aether-gateway/src/ai_pipeline/planner/standard/openai/cli/plans.rs",
"apps/aether-gateway/src/ai_pipeline/planner/standard/openai/responses/plans.rs",
);
assert!(
plans.contains("crate::ai_pipeline::"),
"gateway planner/standard/openai/cli/plans.rs should delegate openai-cli spec resolution through the ai_pipeline root seam"
"gateway planner/standard/openai/responses/plans.rs should delegate openai-responses spec resolution through the ai_pipeline root seam"
);
for forbidden in [
"fn resolve_sync_spec(",
"fn resolve_stream_spec(",
"OPENAI_CLI_SYNC_PLAN_KIND",
"OPENAI_COMPACT_STREAM_PLAN_KIND",
"LocalOpenAiCliSpec {",
"LocalOpenAiResponsesSpec {",
] {
assert!(
!plans.contains(forbidden),
"gateway planner/standard/openai/cli/plans.rs should not keep pure openai-cli resolver detail {forbidden}"
"gateway planner/standard/openai/responses/plans.rs should not keep pure openai-responses resolver detail {forbidden}"
);
}
}

View File

@@ -197,7 +197,7 @@ async fn gateway_refreshes_admin_provider_quota_locally_for_codex_with_trusted_a
);
assert_eq!(
seen_execution_runtime_request.provider_api_format,
"openai:cli"
"openai:responses"
);
assert_eq!(seen_execution_runtime_request.total_ms, Some(30_000));

View File

@@ -566,7 +566,7 @@ async fn gateway_handles_admin_default_body_rules_locally_with_trusted_admin_pri
let upstream_hits = Arc::new(Mutex::new(0usize));
let upstream_hits_clone = Arc::clone(&upstream_hits);
let upstream = Router::new().route(
"/api/admin/endpoints/defaults/openai:cli/body-rules",
"/api/admin/endpoints/defaults/openai:responses/body-rules",
any(move |_request: Request| {
let upstream_hits_inner = Arc::clone(&upstream_hits_clone);
async move {
@@ -582,7 +582,7 @@ async fn gateway_handles_admin_default_body_rules_locally_with_trusted_admin_pri
let response = reqwest::Client::new()
.get(format!(
"{gateway_url}/api/admin/endpoints/defaults/openai:cli/body-rules?provider_type=codex"
"{gateway_url}/api/admin/endpoints/defaults/openai:responses/body-rules?provider_type=codex"
))
.header(crate::constants::GATEWAY_HEADER, "rust-phase3b")
.header(TRUSTED_ADMIN_USER_ID_HEADER, "admin-user-123")
@@ -594,7 +594,7 @@ async fn gateway_handles_admin_default_body_rules_locally_with_trusted_admin_pri
assert_eq!(response.status(), StatusCode::OK);
let payload: serde_json::Value = response.json().await.expect("json body should parse");
assert_eq!(payload["api_format"], "openai:cli");
assert_eq!(payload["api_format"], "openai:responses");
let rules = payload["body_rules"]
.as_array()
.expect("body_rules should be an array");

View File

@@ -329,16 +329,17 @@ async fn gateway_handles_admin_provider_query_models_with_openai_responses_endpo
assert_eq!(response.status(), StatusCode::OK);
let payload: serde_json::Value = response.json().await.expect("json body should parse");
assert_eq!(payload["success"], json!(false));
assert_eq!(
payload["data"]["error"],
json!("No active endpoints found for this provider")
);
assert_eq!(payload["success"], json!(true));
assert_eq!(payload["data"]["error"], serde_json::Value::Null);
assert_eq!(payload["data"]["from_cache"], json!(false));
assert_eq!(payload["data"]["models"], json!([]));
assert_eq!(payload["data"]["models"][0]["id"], json!("gpt-4.1"));
assert_eq!(
payload["data"]["models"][0]["api_formats"],
json!(["openai:responses"])
);
assert_eq!(
*execution_runtime_hits.lock().expect("mutex should lock"),
0
1
);
gateway_handle.abort();
@@ -465,7 +466,7 @@ async fn gateway_handles_admin_provider_query_models_respecting_key_api_formats(
assert_eq!(payload["data"]["from_cache"], json!(false));
assert_eq!(
payload["data"]["models"][0]["api_formats"],
json!(["openai:cli"])
json!(["openai:responses"])
);
assert_eq!(
*execution_runtime_hits.lock().expect("mutex should lock"),
@@ -1596,7 +1597,7 @@ async fn gateway_handles_openai_cli_test_model_locally() {
"choices": [{
"message": {
"role": "assistant",
"content": "Hello from OpenAI CLI"
"content": "Hello from OpenAI Responses"
}
}]
}
@@ -1660,7 +1661,7 @@ async fn gateway_handles_openai_cli_test_model_locally() {
assert_eq!(payload["success"], json!(true));
assert_eq!(
payload["data"]["response"]["choices"][0]["message"]["content"],
json!("Hello from OpenAI CLI")
json!("Hello from OpenAI Responses")
);
gateway_handle.abort();
@@ -2236,7 +2237,7 @@ async fn gateway_handles_openai_cli_test_model_failover_locally() {
"choices": [{
"message": {
"role": "assistant",
"content": "OpenAI CLI failover path succeeded"
"content": "OpenAI Responses failover path succeeded"
}
}]
}
@@ -2299,7 +2300,7 @@ async fn gateway_handles_openai_cli_test_model_failover_locally() {
assert_eq!(payload["total_attempts"], json!(1));
assert_eq!(
payload["data"]["response"]["choices"][0]["message"]["content"],
json!("OpenAI CLI failover path succeeded")
json!("OpenAI Responses failover path succeeded")
);
gateway_handle.abort();

View File

@@ -943,20 +943,20 @@ async fn gateway_creates_admin_provider_locally_with_trusted_admin_principal() {
.await
.expect("endpoints should list");
assert_eq!(endpoints.len(), 3);
let cli_endpoint = endpoints
let responses_endpoint = endpoints
.iter()
.find(|endpoint| endpoint.api_format == "openai:cli")
.expect("cli endpoint should exist");
.find(|endpoint| endpoint.api_format == "openai:responses")
.expect("responses endpoint should exist");
let compact_endpoint = endpoints
.iter()
.find(|endpoint| endpoint.api_format == "openai:compact")
.find(|endpoint| endpoint.api_format == "openai:responses:compact")
.expect("compact endpoint should exist");
let image_endpoint = endpoints
.iter()
.find(|endpoint| endpoint.api_format == "openai:image")
.expect("image endpoint should exist");
assert_eq!(
cli_endpoint.base_url,
responses_endpoint.base_url,
"https://chatgpt.com/backend-api/codex"
);
assert_eq!(
@@ -967,11 +967,11 @@ async fn gateway_creates_admin_provider_locally_with_trusted_admin_principal() {
image_endpoint.base_url,
"https://chatgpt.com/backend-api/codex"
);
assert_eq!(cli_endpoint.max_retries, Some(7));
assert_eq!(responses_endpoint.max_retries, Some(7));
assert_eq!(compact_endpoint.max_retries, Some(7));
assert_eq!(image_endpoint.max_retries, Some(7));
assert_eq!(
cli_endpoint
responses_endpoint
.config
.as_ref()
.and_then(|value| value.get("upstream_stream_policy"))
@@ -986,7 +986,7 @@ async fn gateway_creates_admin_provider_locally_with_trusted_admin_principal() {
.and_then(serde_json::Value::as_str),
Some("force_stream")
);
assert!(cli_endpoint.body_rules.is_none());
assert!(responses_endpoint.body_rules.is_none());
assert!(compact_endpoint.body_rules.is_none());
assert!(image_endpoint.body_rules.is_none());
assert_eq!(*upstream_hits.lock().expect("mutex should lock"), 0);
@@ -1079,24 +1079,24 @@ async fn gateway_updates_fixed_provider_and_reconciles_template_managed_endpoint
.await
.expect("endpoints should list");
assert_eq!(endpoints.len(), 3);
let cli_endpoint = endpoints
let responses_endpoint = endpoints
.iter()
.find(|endpoint| endpoint.api_format == "openai:cli")
.expect("cli endpoint should exist");
.find(|endpoint| endpoint.api_format == "openai:responses")
.expect("responses endpoint should exist");
let compact_endpoint = endpoints
.iter()
.find(|endpoint| endpoint.api_format == "openai:compact")
.find(|endpoint| endpoint.api_format == "openai:responses:compact")
.expect("compact endpoint should exist");
let image_endpoint = endpoints
.iter()
.find(|endpoint| endpoint.api_format == "openai:image")
.expect("image endpoint should exist");
assert_eq!(cli_endpoint.max_retries, Some(9));
assert_eq!(responses_endpoint.max_retries, Some(9));
assert_eq!(compact_endpoint.max_retries, Some(9));
assert_eq!(image_endpoint.max_retries, Some(9));
assert_eq!(
cli_endpoint
responses_endpoint
.config
.as_ref()
.and_then(|value| value.get("_aether_fixed_provider_template"))

View File

@@ -443,7 +443,7 @@ mod tests {
)
.expect("key should build")
.with_transport_fields(
Some(json!(["openai:cli"])),
Some(json!(["openai:responses"])),
"sk-codex-test".to_string(),
None,
None,
@@ -668,7 +668,7 @@ mod tests {
report_context: Some(json!({
"request_id": "req-reporting-sync-null-1",
"client_api_format": "claude:cli",
"provider_api_format": "openai:cli"
"provider_api_format": "openai:responses"
})),
status_code: 200,
headers: BTreeMap::new(),
@@ -758,7 +758,7 @@ mod tests {
&state,
GatewaySyncReportRequest {
trace_id: "trace-codex-reporting-sync".to_string(),
report_kind: "openai_cli_sync_success".to_string(),
report_kind: "openai_responses_sync_success".to_string(),
report_context: Some(json!({
"request_id": "req-codex-reporting-sync",
"key_id": "key-codex-sync"
@@ -821,7 +821,7 @@ mod tests {
&state,
GatewayStreamReportRequest {
trace_id: "trace-codex-reporting-stream".to_string(),
report_kind: "openai_cli_stream_success".to_string(),
report_kind: "openai_responses_stream_success".to_string(),
report_context: Some(json!({
"request_id": "req-codex-reporting-stream",
"key_id": "key-codex-stream"

View File

@@ -839,6 +839,8 @@ fn admin_usage_api_format_defaults_to_non_stream(item: &StoredRequestUsageAudit)
api_format,
Some(value)
if value.eq_ignore_ascii_case("openai:chat")
|| value.eq_ignore_ascii_case("openai:responses")
|| value.eq_ignore_ascii_case("openai:responses:compact")
|| value.eq_ignore_ascii_case("openai:cli")
|| value.eq_ignore_ascii_case("openai:compact")
|| value.eq_ignore_ascii_case("openai:image")
@@ -2333,10 +2335,11 @@ mod tests {
}
#[test]
fn client_requested_stream_defaults_to_non_stream_for_openai_cli_request_body_without_flag() {
fn client_requested_stream_defaults_to_non_stream_for_openai_responses_request_body_without_flag(
) {
let item = StoredRequestUsageAudit {
is_stream: true,
api_format: Some("openai:cli".to_string()),
api_format: Some("openai:responses".to_string()),
request_body: Some(json!({
"model": "gpt-5.4",
"input": [{"role": "user", "content": "hi"}],

View File

@@ -530,16 +530,16 @@ const ADMIN_API_FORMAT_DEFINITIONS: &[AdminApiFormatDefinition] = &[
],
},
AdminApiFormatDefinition {
value: "openai:cli",
label: "OpenAI CLI",
value: "openai:responses",
label: "OpenAI Responses",
default_path: "/v1/responses",
aliases: &["openai_cli", "responses"],
aliases: &["openai_cli", "openai:cli", "responses"],
},
AdminApiFormatDefinition {
value: "openai:compact",
label: "OpenAI Compact",
value: "openai:responses:compact",
label: "OpenAI Responses Compact",
default_path: "/v1/responses/compact",
aliases: &["openai_compact", "responses_compact"],
aliases: &["openai_compact", "openai:compact", "responses_compact"],
},
AdminApiFormatDefinition {
value: "openai:image",

View File

@@ -0,0 +1,13 @@
[package]
name = "aether-ai-formats"
version = "0.1.0"
edition.workspace = true
license.workspace = true
repository.workspace = true
description = "Canonical AI format IR and adapters for Aether"
[dependencies]
regex.workspace = true
serde.workspace = true
serde_json.workspace = true
uuid.workspace = true

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,2 @@
pub mod request;
pub mod response;

View File

@@ -1,8 +1,6 @@
mod claude;
mod gemini;
mod openai_cli;
mod shared;
pub use claude::convert_openai_chat_request_to_claude_request;
pub use gemini::convert_openai_chat_request_to_gemini_request;
pub use openai_cli::convert_openai_chat_request_to_openai_cli_request;

View File

@@ -0,0 +1,20 @@
//! Pairwise request adapters kept for compatibility and focused tests.
//!
//! New request routing should use the registry so every conversion passes
//! through the typed canonical IR.
pub mod from_openai_chat;
pub mod openai_responses;
pub mod to_openai_chat;
pub use from_openai_chat::{
convert_openai_chat_request_to_claude_request, convert_openai_chat_request_to_gemini_request,
};
pub use openai_responses::{
convert_openai_chat_request_to_openai_responses_request,
normalize_openai_responses_request_to_openai_chat_request,
};
pub use to_openai_chat::{
extract_openai_text_content, normalize_claude_request_to_openai_chat_request,
normalize_gemini_request_to_openai_chat_request, parse_openai_tool_result_content,
};

View File

@@ -7,7 +7,7 @@ use crate::planner::openai::{
copy_request_number_field, extract_openai_reasoning_effort, value_as_u64,
};
pub fn convert_openai_chat_request_to_openai_cli_request(
pub fn convert_openai_chat_request_to_openai_responses_request(
body_json: &Value,
mapped_model: &str,
upstream_is_stream: bool,
@@ -36,7 +36,7 @@ pub fn convert_openai_chat_request_to_openai_cli_request(
}
}
"user" | "assistant" => {
let mut content_items = convert_openai_content_to_openai_cli_items(
let mut content_items = convert_openai_content_to_openai_responses_items(
message_object.get("content"),
role.as_str(),
)?;
@@ -197,20 +197,21 @@ pub fn convert_openai_chat_request_to_openai_cli_request(
}
}
if let Some(text) = build_openai_cli_text_config_from_openai_chat_request(request) {
if let Some(text) = build_openai_responses_text_config_from_openai_chat_request(request) {
output.insert("text".to_string(), Value::Object(text));
}
if let Some(tools) = build_openai_cli_tools_from_openai_chat_request(request) {
if let Some(tools) = build_openai_responses_tools_from_openai_chat_request(request) {
output.insert("tools".to_string(), Value::Array(tools));
}
if let Some(tool_choice) = build_openai_cli_tool_choice_from_openai_chat_request(request) {
if let Some(tool_choice) = build_openai_responses_tool_choice_from_openai_chat_request(request)
{
output.insert("tool_choice".to_string(), tool_choice);
}
Some(Value::Object(output))
}
fn convert_openai_content_to_openai_cli_items(
fn convert_openai_content_to_openai_responses_items(
content: Option<&Value>,
role: &str,
) -> Option<Vec<Value>> {
@@ -218,6 +219,7 @@ fn convert_openai_content_to_openai_cli_items(
return Some(Vec::new());
};
match content {
Value::Null => Some(Vec::new()),
Value::String(text) => {
if text.is_empty() {
Some(Vec::new())
@@ -337,7 +339,7 @@ fn convert_openai_content_to_openai_cli_items(
}
}
fn build_openai_cli_text_config_from_openai_chat_request(
fn build_openai_responses_text_config_from_openai_chat_request(
request: &Map<String, Value>,
) -> Option<Map<String, Value>> {
let mut text = Map::new();
@@ -350,7 +352,7 @@ fn build_openai_cli_text_config_from_openai_chat_request(
(!text.is_empty()).then_some(text)
}
fn build_openai_cli_tools_from_openai_chat_request(
fn build_openai_responses_tools_from_openai_chat_request(
request: &Map<String, Value>,
) -> Option<Vec<Value>> {
let mut tools = Vec::new();
@@ -437,7 +439,7 @@ fn build_openai_cli_tools_from_openai_chat_request(
(!tools.is_empty()).then_some(tools)
}
fn build_openai_cli_tool_choice_from_openai_chat_request(
fn build_openai_responses_tool_choice_from_openai_chat_request(
request: &Map<String, Value>,
) -> Option<Value> {
let tool_choice = request.get("tool_choice")?;
@@ -504,11 +506,11 @@ fn copy_request_bool_field(
#[cfg(test)]
mod tests {
use super::convert_openai_chat_request_to_openai_cli_request;
use super::convert_openai_chat_request_to_openai_responses_request;
use serde_json::json;
#[test]
fn preserves_shared_openai_chat_controls_when_converting_to_openai_cli() {
fn preserves_shared_openai_chat_controls_when_converting_to_openai_responses() {
let request = json!({
"model": "gpt-5",
"messages": [{"role": "user", "content": "hi"}],
@@ -522,7 +524,7 @@ mod tests {
"top_logprobs": 3,
});
let converted = convert_openai_chat_request_to_openai_cli_request(
let converted = convert_openai_chat_request_to_openai_responses_request(
&request,
"gpt-5-upstream",
false,
@@ -542,7 +544,7 @@ mod tests {
}
#[test]
fn preserves_assistant_refusal_when_converting_to_openai_cli() {
fn preserves_assistant_refusal_when_converting_to_openai_responses() {
let request = json!({
"model": "gpt-5",
"messages": [{
@@ -552,7 +554,7 @@ mod tests {
}]
});
let converted = convert_openai_chat_request_to_openai_cli_request(
let converted = convert_openai_chat_request_to_openai_responses_request(
&request,
"gpt-5-upstream",
false,

View File

@@ -0,0 +1,54 @@
mod from_chat;
mod to_chat;
pub use from_chat::convert_openai_chat_request_to_openai_responses_request;
pub use to_chat::normalize_openai_responses_request_to_openai_chat_request;
#[cfg(test)]
mod tests {
use super::{
convert_openai_chat_request_to_openai_responses_request,
normalize_openai_responses_request_to_openai_chat_request,
};
use serde_json::json;
#[test]
fn converts_chat_to_responses_wire_shape() {
let request = json!({
"model": "gpt-5",
"messages": [{"role": "user", "content": "hello"}],
"max_completion_tokens": 16
});
let converted = convert_openai_chat_request_to_openai_responses_request(
&request,
"gpt-5-mini",
false,
false,
)
.expect("responses request");
assert_eq!(converted["model"], "gpt-5-mini");
assert_eq!(converted["input"][0]["type"], "message");
assert_eq!(converted["max_output_tokens"], 16);
}
#[test]
fn normalizes_responses_wire_shape_to_chat() {
let request = json!({
"model": "gpt-5",
"input": [{
"type": "message",
"role": "user",
"content": [{"type": "input_text", "text": "hello"}]
}]
});
let converted = normalize_openai_responses_request_to_openai_chat_request(&request)
.expect("chat request");
assert_eq!(converted["messages"][0]["role"], "user");
assert_eq!(converted["messages"][0]["content"][0]["type"], "text");
assert_eq!(converted["messages"][0]["content"][0]["text"], "hello");
}
}

View File

@@ -1,9 +1,11 @@
use serde_json::{json, Map, Value};
use super::shared::{extract_openai_text_content, parse_openai_tool_result_content};
use super::super::to_openai_chat::{extract_openai_text_content, parse_openai_tool_result_content};
use crate::planner::openai::extract_openai_reasoning_effort;
pub fn normalize_openai_cli_request_to_openai_chat_request(body_json: &Value) -> Option<Value> {
pub fn normalize_openai_responses_request_to_openai_chat_request(
body_json: &Value,
) -> Option<Value> {
let request = body_json.as_object()?;
let mut output = Map::new();
if let Some(model) = request.get("model") {
@@ -20,7 +22,7 @@ pub fn normalize_openai_cli_request_to_openai_chat_request(body_json: &Value) ->
}));
}
}
messages.extend(normalize_openai_cli_input_to_openai_chat_messages(
messages.extend(normalize_openai_responses_input_to_openai_chat_messages(
request.get("input"),
)?);
output.insert("messages".to_string(), Value::Array(messages));
@@ -70,16 +72,16 @@ pub fn normalize_openai_cli_request_to_openai_chat_request(body_json: &Value) ->
{
output.insert("verbosity".to_string(), verbosity);
}
if let Some(tools) = normalize_openai_cli_tools_to_openai_chat(request.get("tools"))? {
if let Some(tools) = normalize_openai_responses_tools_to_openai_chat(request.get("tools"))? {
output.insert("tools".to_string(), Value::Array(tools));
}
if let Some(web_search_options) =
extract_openai_cli_web_search_options(request.get("tools").and_then(Value::as_array))
extract_openai_responses_web_search_options(request.get("tools").and_then(Value::as_array))
{
output.insert("web_search_options".to_string(), web_search_options);
}
if let Some(tool_choice) =
normalize_openai_cli_tool_choice_to_openai_chat(request.get("tool_choice"))?
normalize_openai_responses_tool_choice_to_openai_chat(request.get("tool_choice"))?
{
output.insert("tool_choice".to_string(), tool_choice);
}
@@ -87,7 +89,9 @@ pub fn normalize_openai_cli_request_to_openai_chat_request(body_json: &Value) ->
Some(Value::Object(output))
}
fn normalize_openai_cli_input_to_openai_chat_messages(input: Option<&Value>) -> Option<Vec<Value>> {
fn normalize_openai_responses_input_to_openai_chat_messages(
input: Option<&Value>,
) -> Option<Vec<Value>> {
let Some(input) = input else {
return Some(Vec::new());
};
@@ -142,14 +146,14 @@ fn normalize_openai_cli_input_to_openai_chat_messages(input: Option<&Value>) ->
continue;
}
let normalized_content =
normalize_openai_cli_message_content(item_object.get("content"))?;
normalize_openai_responses_message_content(item_object.get("content"))?;
let mut message = serde_json::Map::new();
message.insert("role".to_string(), Value::String(role.clone()));
message.insert("content".to_string(), normalized_content);
if role == "assistant" {
if let Some(refusal) =
extract_openai_cli_message_refusal(item_object.get("content"))?
{
if let Some(refusal) = extract_openai_responses_message_refusal(
item_object.get("content"),
)? {
message.insert("refusal".to_string(), Value::String(refusal));
}
}
@@ -222,7 +226,7 @@ fn normalize_openai_cli_input_to_openai_chat_messages(input: Option<&Value>) ->
}
}
fn normalize_openai_cli_message_content(content: Option<&Value>) -> Option<Value> {
fn normalize_openai_responses_message_content(content: Option<&Value>) -> Option<Value> {
let Some(content) = content else {
return Some(Value::Array(Vec::new()));
};
@@ -314,7 +318,7 @@ fn normalize_openai_cli_message_content(content: Option<&Value>) -> Option<Value
}
}
fn extract_openai_cli_message_refusal(content: Option<&Value>) -> Option<Option<String>> {
fn extract_openai_responses_message_refusal(content: Option<&Value>) -> Option<Option<String>> {
let Some(content) = content else {
return Some(None);
};
@@ -347,7 +351,9 @@ fn extract_openai_cli_message_refusal(content: Option<&Value>) -> Option<Option<
}
}
fn normalize_openai_cli_tools_to_openai_chat(tools: Option<&Value>) -> Option<Option<Vec<Value>>> {
fn normalize_openai_responses_tools_to_openai_chat(
tools: Option<&Value>,
) -> Option<Option<Vec<Value>>> {
let Some(Value::Array(tool_values)) = tools else {
return Some(None);
};
@@ -387,7 +393,7 @@ fn normalize_openai_cli_tools_to_openai_chat(tools: Option<&Value>) -> Option<Op
Some((!normalized.is_empty()).then_some(normalized))
}
fn extract_openai_cli_web_search_options(tools: Option<&Vec<Value>>) -> Option<Value> {
fn extract_openai_responses_web_search_options(tools: Option<&Vec<Value>>) -> Option<Value> {
let tool_values = tools?;
for tool in tool_values {
let tool_object = tool.as_object()?;
@@ -428,7 +434,7 @@ fn extract_openai_cli_web_search_options(tools: Option<&Vec<Value>>) -> Option<V
None
}
fn normalize_openai_cli_tool_choice_to_openai_chat(
fn normalize_openai_responses_tool_choice_to_openai_chat(
tool_choice: Option<&Value>,
) -> Option<Option<Value>> {
let Some(tool_choice) = tool_choice else {
@@ -460,11 +466,11 @@ fn normalize_openai_cli_tool_choice_to_openai_chat(
#[cfg(test)]
mod tests {
use super::normalize_openai_cli_request_to_openai_chat_request;
use super::normalize_openai_responses_request_to_openai_chat_request;
use serde_json::json;
#[test]
fn preserves_openai_cli_text_and_passthrough_fields_when_normalizing_to_chat() {
fn preserves_openai_responses_text_and_passthrough_fields_when_normalizing_to_chat() {
let request = json!({
"model": "gpt-5",
"max_output_tokens": 128,
@@ -487,7 +493,7 @@ mod tests {
"top_logprobs": 4
});
let converted = normalize_openai_cli_request_to_openai_chat_request(&request)
let converted = normalize_openai_responses_request_to_openai_chat_request(&request)
.expect("responses request should normalize to chat");
assert_eq!(converted["max_completion_tokens"], 128);
@@ -518,7 +524,7 @@ mod tests {
}]
});
let converted = normalize_openai_cli_request_to_openai_chat_request(&request)
let converted = normalize_openai_responses_request_to_openai_chat_request(&request)
.expect("responses request should normalize to chat");
assert_eq!(converted["messages"][0]["role"], "assistant");
@@ -537,7 +543,7 @@ mod tests {
"input": "hello"
});
let converted = normalize_openai_cli_request_to_openai_chat_request(&request)
let converted = normalize_openai_responses_request_to_openai_chat_request(&request)
.expect("responses request should normalize to chat");
assert_eq!(converted["stream"], true);
@@ -556,7 +562,7 @@ mod tests {
"input": "hello"
});
let converted = normalize_openai_cli_request_to_openai_chat_request(&request)
let converted = normalize_openai_responses_request_to_openai_chat_request(&request)
.expect("responses request should normalize to chat");
assert_eq!(converted["stream_options"]["include_usage"], false);

View File

@@ -1,9 +1,7 @@
mod claude;
mod gemini;
mod openai_cli;
mod shared;
pub use claude::normalize_claude_request_to_openai_chat_request;
pub use gemini::normalize_gemini_request_to_openai_chat_request;
pub use openai_cli::normalize_openai_cli_request_to_openai_chat_request;
pub use shared::{extract_openai_text_content, parse_openai_tool_result_content};

Some files were not shown because too many files have changed in this diff Show More