mirror of
https://github.com/fawney19/Aether.git
synced 2026-09-01 08:50:21 +08:00
migrate ai format conversion to responses adapters
This commit is contained in:
12
Cargo.lock
generated
12
Cargo.lock
generated
@@ -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",
|
||||
|
||||
@@ -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" }
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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"]
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
@@ -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(
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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",
|
||||
});
|
||||
|
||||
@@ -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,
|
||||
)
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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");
|
||||
}
|
||||
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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(),
|
||||
);
|
||||
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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)?
|
||||
|
||||
@@ -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,
|
||||
};
|
||||
|
||||
@@ -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?
|
||||
|
||||
@@ -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?
|
||||
|
||||
@@ -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 => (
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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")
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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,
|
||||
};
|
||||
|
||||
@@ -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"}"#),
|
||||
);
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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());
|
||||
|
||||
@@ -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,
|
||||
};
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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,
|
||||
@@ -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",
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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;
|
||||
@@ -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,
|
||||
};
|
||||
|
||||
@@ -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,
|
||||
};
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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;
|
||||
@@ -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,
|
||||
@@ -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,
|
||||
@@ -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,
|
||||
@@ -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
|
||||
@@ -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"
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -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"],
|
||||
|
||||
@@ -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,
|
||||
};
|
||||
|
||||
@@ -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> {
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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!(
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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?;
|
||||
|
||||
@@ -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()),
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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?
|
||||
|
||||
@@ -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?
|
||||
|
||||
@@ -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 端点",
|
||||
_ => "找不到有效端点",
|
||||
|
||||
@@ -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))
|
||||
|
||||
@@ -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),
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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(
|
||||
|
||||
@@ -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(
|
||||
|
||||
@@ -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");
|
||||
|
||||
@@ -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(),
|
||||
|
||||
@@ -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"],
|
||||
_ => &[],
|
||||
}
|
||||
|
||||
@@ -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"}],
|
||||
|
||||
@@ -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"));
|
||||
}
|
||||
|
||||
@@ -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")
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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()]
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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}"
|
||||
))
|
||||
}
|
||||
|
||||
@@ -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 认证上下文,无法选择上游提供商",
|
||||
)
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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}"
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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));
|
||||
|
||||
|
||||
@@ -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");
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -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"))
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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"}],
|
||||
|
||||
@@ -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",
|
||||
|
||||
13
crates/aether-ai-formats/Cargo.toml
Normal file
13
crates/aether-ai-formats/Cargo.toml
Normal 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
|
||||
5870
crates/aether-ai-formats/src/canonical.rs
Normal file
5870
crates/aether-ai-formats/src/canonical.rs
Normal file
File diff suppressed because it is too large
Load Diff
2
crates/aether-ai-formats/src/conversion/mod.rs
Normal file
2
crates/aether-ai-formats/src/conversion/mod.rs
Normal file
@@ -0,0 +1,2 @@
|
||||
pub mod request;
|
||||
pub mod response;
|
||||
@@ -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;
|
||||
20
crates/aether-ai-formats/src/conversion/request/mod.rs
Normal file
20
crates/aether-ai-formats/src/conversion/request/mod.rs
Normal 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,
|
||||
};
|
||||
@@ -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,
|
||||
@@ -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");
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
@@ -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
Reference in New Issue
Block a user