Normalize canonical API formats

This commit is contained in:
fawney19
2026-04-29 10:20:41 +08:00
parent 02ad67fe33
commit 07a319259b
171 changed files with 2460 additions and 1962 deletions
@@ -39,8 +39,8 @@ fn encode_frame(headers: Vec<u8>, payload: Vec<u8>) -> Vec<u8> {
fn kiro_report_context(thinking_enabled: bool) -> Value {
let mut context = json!({
"provider_api_format": "claude:cli",
"client_api_format": "claude:cli",
"provider_api_format": "claude:messages",
"client_api_format": "claude:messages",
"envelope_name": "kiro:generateAssistantResponse",
"mapped_model": "claude-sonnet-4.5"
});
@@ -9,7 +9,7 @@ fn normalizes_supported_private_report_context() {
let report_context = json!({
"has_envelope": true,
"envelope_name": "antigravity:v1internal",
"provider_api_format": "gemini:cli",
"provider_api_format": "gemini:generate_content",
});
let normalized = normalize_provider_private_report_context(Some(&report_context))
.expect("context should normalize");
@@ -21,8 +21,8 @@ fn normalizes_supported_private_report_context() {
fn private_stream_normalizer_unwraps_antigravity_stream() {
let report_context = json!({
"has_envelope": true,
"provider_api_format": "gemini:cli",
"client_api_format": "gemini:cli",
"provider_api_format": "gemini:generate_content",
"client_api_format": "gemini:generate_content",
"envelope_name": "antigravity:v1internal",
"mapped_model": "claude-sonnet-4-5",
});
@@ -30,7 +30,7 @@ mod tests {
Some(RequestConversionKind::ToOpenAiResponses)
);
assert_eq!(
request_conversion_kind("openai:chat", "claude:cli"),
request_conversion_kind("openai:chat", "claude:messages"),
Some(RequestConversionKind::ToClaudeStandard)
);
assert_eq!(
@@ -38,56 +38,59 @@ mod tests {
Some(RequestConversionKind::ToOpenAIChat)
);
assert_eq!(
request_conversion_kind("gemini:chat", "claude:chat"),
request_conversion_kind("gemini:generate_content", "claude:messages"),
Some(RequestConversionKind::ToClaudeStandard)
);
assert_eq!(
request_conversion_kind("openai:responses:compact", "gemini:cli"),
request_conversion_kind("openai:responses:compact", "gemini:generate_content"),
None
);
assert_eq!(
request_conversion_kind("gemini:cli", "openai:responses:compact"),
request_conversion_kind("gemini:generate_content", "openai:responses:compact"),
None
);
assert_eq!(
request_conversion_kind("openai:chat", "openai:responses:compact"),
None
);
assert_eq!(request_conversion_kind("claude:chat", "claude:chat"), None);
assert_eq!(
request_conversion_kind("claude:messages", "claude:messages"),
None
);
}
#[test]
fn sync_response_conversion_registry_supports_bidirectional_standard_matrix() {
assert_eq!(
sync_chat_response_conversion_kind("openai:chat", "claude:chat"),
sync_chat_response_conversion_kind("openai:chat", "claude:messages"),
Some(SyncChatResponseConversionKind::ToClaudeChat)
);
assert_eq!(
sync_chat_response_conversion_kind("claude:chat", "gemini:chat"),
sync_chat_response_conversion_kind("claude:messages", "gemini:generate_content"),
Some(SyncChatResponseConversionKind::ToGeminiChat)
);
assert_eq!(
sync_chat_response_conversion_kind("gemini:chat", "openai:chat"),
sync_chat_response_conversion_kind("gemini:generate_content", "openai:chat"),
Some(SyncChatResponseConversionKind::ToOpenAIChat)
);
assert_eq!(
sync_cli_response_conversion_kind("openai:responses", "gemini:cli"),
sync_cli_response_conversion_kind("openai:responses", "gemini:generate_content"),
Some(SyncCliResponseConversionKind::ToGeminiCli)
);
assert_eq!(
sync_cli_response_conversion_kind("claude:chat", "openai:responses"),
sync_cli_response_conversion_kind("claude:messages", "openai:responses"),
Some(SyncCliResponseConversionKind::ToOpenAiResponses)
);
assert_eq!(
sync_cli_response_conversion_kind("claude:cli", "openai:responses:compact"),
sync_cli_response_conversion_kind("claude:messages", "openai:responses:compact"),
Some(SyncCliResponseConversionKind::ToOpenAiResponses)
);
assert_eq!(
sync_cli_response_conversion_kind("openai:responses:compact", "claude:cli"),
sync_cli_response_conversion_kind("openai:responses:compact", "claude:messages"),
None
);
assert_eq!(
sync_cli_response_conversion_kind("gemini:cli", "claude:cli"),
sync_cli_response_conversion_kind("gemini:generate_content", "claude:messages"),
Some(SyncCliResponseConversionKind::ToClaudeCli)
);
}
@@ -98,49 +101,31 @@ mod tests {
request_candidate_api_formats("openai:chat", false),
vec![
"openai:chat",
"claude:chat",
"gemini:chat",
"openai:responses",
"claude:cli",
"gemini:cli",
"claude:messages",
"gemini:generate_content",
]
);
assert_eq!(
request_candidate_api_formats("openai:responses", false),
vec![
"openai:responses",
"claude:cli",
"gemini:cli",
"openai:chat",
"claude:chat",
"gemini:chat",
"claude:messages",
"gemini:generate_content",
]
);
assert_eq!(
request_candidate_api_formats("openai:cli", false),
vec![
"openai:responses",
"claude:cli",
"gemini:cli",
"openai:chat",
"claude:chat",
"gemini:chat",
]
Vec::<&'static str>::new()
);
assert_eq!(
request_candidate_api_formats("claude:cli", false),
vec![
"claude:cli",
"openai:responses",
"gemini:cli",
"claude:chat",
"openai:chat",
"gemini:chat",
]
Vec::<&'static str>::new()
);
assert_eq!(
request_candidate_api_formats("openai:compact", false),
vec!["openai:responses:compact"]
Vec::<&'static str>::new()
);
}
@@ -148,15 +133,15 @@ mod tests {
fn request_candidate_registry_prefers_same_kind_before_same_family_fallbacks() {
assert_eq!(
request_candidate_api_format_preference("claude:cli", "openai:responses"),
Some((1, 0))
None
);
assert_eq!(
request_candidate_api_format_preference("claude:cli", "claude:chat"),
Some((2, 1))
None
);
assert_eq!(
request_candidate_api_format_preference("claude:cli", "openai:chat"),
Some((3, 0))
None
);
}
}
@@ -110,7 +110,7 @@ fn maybe_bridge_openai_image_sync_json_to_stream(
}
fn normalize_api_format(value: &str) -> String {
crate::ai_pipeline::normalize_legacy_openai_format_alias(value)
crate::ai_pipeline::normalize_api_format_alias(value)
}
fn is_standard_api_format(value: &str) -> bool {
@@ -119,10 +119,8 @@ fn is_standard_api_format(value: &str) -> bool {
"openai:chat"
| "openai:responses"
| "openai:responses:compact"
| "claude:chat"
| "claude:cli"
| "gemini:chat"
| "gemini:cli"
| "claude:messages"
| "gemini:generate_content"
)
}
@@ -210,10 +208,10 @@ fn convert_provider_sync_response_to_openai_responses(
report_context,
false,
),
"claude:chat" | "claude:cli" => {
"claude:messages" => {
convert_claude_response_to_openai_responses(provider_body_json, report_context)
}
"gemini:chat" | "gemini:cli" => {
"gemini:generate_content" => {
convert_gemini_response_to_openai_responses(provider_body_json, report_context)
}
_ => None,
@@ -257,11 +255,11 @@ fn emit_client_stream_from_canonical_frames(
let mut emitter = OpenAIResponsesClientEmitter::default();
emit_with_openai_responses_emitter(&mut emitter, canonical_frames)
}
"claude:chat" | "claude:cli" => {
"claude:messages" => {
let mut emitter = ClaudeClientEmitter::default();
emit_with_claude_emitter(&mut emitter, canonical_frames)
}
"gemini:chat" | "gemini:cli" => {
"gemini:generate_content" => {
let mut emitter = GeminiClientEmitter::default();
emit_with_gemini_emitter(&mut emitter, canonical_frames)
}
@@ -79,7 +79,7 @@ fn standard_sync_bridge_converts_claude_sync_json_to_openai_responses_sse() {
"output_tokens": 3
}
}),
"claude:chat",
"claude:messages",
"openai:responses",
Some(&report_context),
)
@@ -111,8 +111,8 @@ fn standard_sync_bridge_converts_claude_sync_json_to_openai_responses_sse() {
fn antigravity_stream_rewriter_unwraps_and_injects_tool_ids() {
let report_context = json!({
"has_envelope": true,
"provider_api_format": "gemini:cli",
"client_api_format": "gemini:cli",
"provider_api_format": "gemini:generate_content",
"client_api_format": "gemini:generate_content",
"envelope_name": "antigravity:v1internal",
"needs_conversion": false,
"mapped_model": "claude-sonnet-4-5",
@@ -378,8 +378,8 @@ fn openai_image_stream_rewriter_emits_partial_and_completed_events_for_edit() {
fn gemini_cli_v1internal_stream_rewriter_unwraps_response_object() {
let report_context = json!({
"has_envelope": true,
"provider_api_format": "gemini:cli",
"client_api_format": "gemini:cli",
"provider_api_format": "gemini:generate_content",
"client_api_format": "gemini:generate_content",
"envelope_name": "gemini_cli:v1internal",
"needs_conversion": false,
});
@@ -424,7 +424,7 @@ fn openai_chat_error_to_openai_responses_stream_rewriter_converts_to_response_fa
#[test]
fn claude_error_to_openai_responses_stream_rewriter_converts_to_response_failed() {
let report_context = json!({
"provider_api_format": "claude:chat",
"provider_api_format": "claude:messages",
"client_api_format": "openai:responses",
"needs_conversion": true,
"mapped_model": "claude-sonnet-4-5",
@@ -452,7 +452,7 @@ fn claude_error_to_openai_responses_stream_rewriter_converts_to_response_failed(
#[test]
fn gemini_error_to_openai_responses_stream_rewriter_converts_to_response_failed() {
let report_context = json!({
"provider_api_format": "gemini:cli",
"provider_api_format": "gemini:generate_content",
"client_api_format": "openai:responses",
"needs_conversion": true,
"mapped_model": "gemini-2.5-pro",
@@ -477,7 +477,7 @@ fn gemini_error_to_openai_responses_stream_rewriter_converts_to_response_failed(
fn openai_chat_error_to_claude_chat_stream_rewriter_uses_error_event_line() {
let report_context = json!({
"provider_api_format": "openai:chat",
"client_api_format": "claude:chat",
"client_api_format": "claude:messages",
"needs_conversion": true,
"mapped_model": "gpt-5.4",
});
@@ -501,7 +501,7 @@ fn openai_chat_error_to_claude_chat_stream_rewriter_uses_error_event_line() {
fn openai_chat_error_to_gemini_chat_stream_rewriter_keeps_data_only_error() {
let report_context = json!({
"provider_api_format": "openai:chat",
"client_api_format": "gemini:chat",
"client_api_format": "gemini:generate_content",
"needs_conversion": true,
"mapped_model": "gpt-5.4",
});
@@ -524,7 +524,7 @@ fn openai_chat_error_to_gemini_chat_stream_rewriter_keeps_data_only_error() {
#[test]
fn claude_to_openai_chat_stream_rewriter_converts_text_deltas() {
let report_context = json!({
"provider_api_format": "claude:chat",
"provider_api_format": "claude:messages",
"client_api_format": "openai:chat",
"needs_conversion": true,
"mapped_model": "claude-sonnet-4-5",
@@ -557,7 +557,7 @@ fn claude_to_openai_chat_stream_rewriter_converts_text_deltas() {
#[test]
fn claude_to_openai_chat_stream_rewriter_converts_tool_use_to_tool_calls() {
let report_context = json!({
"provider_api_format": "claude:chat",
"provider_api_format": "claude:messages",
"client_api_format": "openai:chat",
"needs_conversion": true,
"mapped_model": "claude-sonnet-4-5",
@@ -597,7 +597,7 @@ fn claude_to_openai_chat_stream_rewriter_converts_tool_use_to_tool_calls() {
#[test]
fn gemini_to_openai_chat_stream_rewriter_buffers_and_converts_text() {
let report_context = json!({
"provider_api_format": "gemini:chat",
"provider_api_format": "gemini:generate_content",
"client_api_format": "openai:chat",
"needs_conversion": true,
"mapped_model": "gemini-2.5-pro",
@@ -630,7 +630,7 @@ fn gemini_to_openai_chat_stream_rewriter_buffers_and_converts_text() {
#[test]
fn gemini_to_openai_chat_stream_rewriter_buffers_and_converts_function_call() {
let report_context = json!({
"provider_api_format": "gemini:chat",
"provider_api_format": "gemini:generate_content",
"client_api_format": "openai:chat",
"needs_conversion": true,
"mapped_model": "gemini-2.5-pro",
@@ -761,7 +761,7 @@ fn openai_responses_to_openai_chat_stream_rewriter_converts_completed_event_with
#[test]
fn antigravity_gemini_to_openai_chat_stream_rewriter_unwraps_and_converts_function_call() {
let report_context = json!({
"provider_api_format": "gemini:chat",
"provider_api_format": "gemini:generate_content",
"client_api_format": "openai:chat",
"needs_conversion": true,
"has_envelope": true,
@@ -788,7 +788,7 @@ fn antigravity_gemini_to_openai_chat_stream_rewriter_unwraps_and_converts_functi
#[test]
fn antigravity_gemini_to_openai_responses_stream_rewriter_unwraps_and_converts_function_call() {
let report_context = json!({
"provider_api_format": "gemini:cli",
"provider_api_format": "gemini:generate_content",
"client_api_format": "openai:responses",
"needs_conversion": true,
"envelope_name": "antigravity:v1internal",
@@ -815,7 +815,7 @@ fn antigravity_gemini_to_openai_responses_stream_rewriter_unwraps_and_converts_f
#[test]
fn gemini_to_openai_responses_stream_rewriter_buffers_and_converts_to_completed_event() {
let report_context = json!({
"provider_api_format": "gemini:cli",
"provider_api_format": "gemini:generate_content",
"client_api_format": "openai:responses",
"needs_conversion": true,
"mapped_model": "gemini-2.5-pro",
@@ -850,7 +850,7 @@ fn gemini_to_openai_responses_stream_rewriter_buffers_and_converts_to_completed_
#[test]
fn claude_to_openai_responses_stream_rewriter_buffers_and_converts_to_completed_event() {
let report_context = json!({
"provider_api_format": "claude:cli",
"provider_api_format": "claude:messages",
"client_api_format": "openai:responses",
"needs_conversion": true,
"mapped_model": "claude-sonnet-4-5",
@@ -888,7 +888,7 @@ fn claude_to_openai_responses_stream_rewriter_buffers_and_converts_to_completed_
#[test]
fn claude_to_openai_responses_stream_rewriter_converts_tool_use_to_function_call() {
let report_context = json!({
"provider_api_format": "claude:cli",
"provider_api_format": "claude:messages",
"client_api_format": "openai:responses",
"needs_conversion": true,
"mapped_model": "claude-sonnet-4-5",
@@ -934,7 +934,7 @@ fn claude_to_openai_responses_stream_rewriter_converts_tool_use_to_function_call
#[test]
fn gemini_to_openai_responses_stream_rewriter_converts_function_call_to_completed_event() {
let report_context = json!({
"provider_api_format": "gemini:cli",
"provider_api_format": "gemini:generate_content",
"client_api_format": "openai:responses",
"needs_conversion": true,
"mapped_model": "gemini-2.5-pro",
@@ -961,7 +961,7 @@ fn gemini_to_openai_responses_stream_rewriter_converts_function_call_to_complete
#[test]
fn gemini_to_openai_responses_compact_stream_rewriter_converts_function_call_to_completed_event() {
let report_context = json!({
"provider_api_format": "gemini:cli",
"provider_api_format": "gemini:generate_content",
"client_api_format": "openai:responses:compact",
"needs_conversion": true,
"mapped_model": "gemini-2.5-pro",
@@ -989,7 +989,7 @@ fn gemini_to_openai_responses_compact_stream_rewriter_converts_function_call_to_
fn openai_chat_to_claude_chat_stream_rewriter_converts_via_standard_matrix() {
let report_context = json!({
"provider_api_format": "openai:chat",
"client_api_format": "claude:chat",
"client_api_format": "claude:messages",
"needs_conversion": true,
"mapped_model": "gpt-5",
});
@@ -1029,7 +1029,7 @@ fn openai_chat_to_claude_chat_stream_rewriter_injects_default_usage_when_finish_
{
let report_context = json!({
"provider_api_format": "openai:chat",
"client_api_format": "claude:chat",
"client_api_format": "claude:messages",
"needs_conversion": true,
"mapped_model": "gpt-5.4",
});
@@ -1066,7 +1066,7 @@ fn openai_chat_to_claude_chat_stream_rewriter_injects_default_usage_when_finish_
fn openai_chat_to_gemini_cli_stream_rewriter_converts_via_standard_matrix() {
let report_context = json!({
"provider_api_format": "openai:chat",
"client_api_format": "gemini:cli",
"client_api_format": "gemini:generate_content",
"needs_conversion": true,
"mapped_model": "gpt-5",
});
@@ -94,8 +94,8 @@ fn build_kiro_claude_cli_sync_finalize_payload(body_bytes: Vec<u8>) -> GatewaySy
trace_id: "trace-kiro-cli-sync-local-finalize-123".to_string(),
report_kind: "claude_cli_sync_finalize".to_string(),
report_context: Some(json!({
"client_api_format": "claude:cli",
"provider_api_format": "claude:cli",
"client_api_format": "claude:messages",
"provider_api_format": "claude:messages",
"model": "claude-sonnet-4",
"mapped_model": "claude-sonnet-4-upstream",
"needs_conversion": false,
@@ -332,7 +332,7 @@ fn converts_claude_chat_response_to_openai_chat_response() {
}),
&json!({
"client_api_format": "openai:chat",
"provider_api_format": "claude:chat",
"provider_api_format": "claude:messages",
"model": "gpt-5"
}),
)
@@ -386,7 +386,7 @@ fn converts_claude_chat_tool_use_to_openai_chat_tool_calls() {
}),
&json!({
"client_api_format": "openai:chat",
"provider_api_format": "claude:chat",
"provider_api_format": "claude:messages",
"model": "gpt-5"
}),
)
@@ -443,7 +443,7 @@ fn converts_claude_chat_thinking_block_to_openai_reasoning_content() {
}),
&json!({
"client_api_format": "openai:chat",
"provider_api_format": "claude:chat",
"provider_api_format": "claude:messages",
"model": "gpt-5"
}),
)
@@ -478,7 +478,7 @@ fn converts_gemini_chat_response_to_openai_chat_response() {
}),
&json!({
"client_api_format": "openai:chat",
"provider_api_format": "gemini:chat",
"provider_api_format": "gemini:generate_content",
"model": "gpt-5"
}),
)
@@ -532,7 +532,7 @@ fn converts_gemini_chat_function_call_to_openai_chat_tool_calls() {
}),
&json!({
"client_api_format": "openai:chat",
"provider_api_format": "gemini:chat",
"provider_api_format": "gemini:generate_content",
"model": "gpt-5"
}),
)
@@ -594,7 +594,7 @@ fn converts_gemini_chat_thought_part_to_openai_reasoning_content() {
}),
&json!({
"client_api_format": "openai:chat",
"provider_api_format": "gemini:chat",
"provider_api_format": "gemini:generate_content",
"model": "gpt-5"
}),
)
@@ -634,7 +634,7 @@ fn converts_gemini_chat_inline_data_to_openai_chat_image_part() {
}),
&json!({
"client_api_format": "openai:chat",
"provider_api_format": "gemini:chat",
"provider_api_format": "gemini:generate_content",
"model": "gpt-5"
}),
)
@@ -805,7 +805,7 @@ fn converts_claude_cli_response_to_openai_responses_response() {
}),
&json!({
"client_api_format": "openai:responses",
"provider_api_format": "claude:cli",
"provider_api_format": "claude:messages",
"model": "gpt-5"
}),
)
@@ -862,7 +862,7 @@ fn converts_claude_cli_tool_use_to_openai_responses_function_call() {
}),
&json!({
"client_api_format": "openai:responses",
"provider_api_format": "claude:cli",
"provider_api_format": "claude:messages",
"model": "gpt-5"
}),
)
@@ -927,7 +927,7 @@ fn converts_gemini_cli_response_to_openai_responses_response() {
}),
&json!({
"client_api_format": "openai:responses",
"provider_api_format": "gemini:cli",
"provider_api_format": "gemini:generate_content",
"model": "gpt-5"
}),
)
@@ -989,7 +989,7 @@ fn converts_gemini_cli_function_call_to_openai_responses_function_call() {
}),
&json!({
"client_api_format": "openai:responses",
"provider_api_format": "gemini:cli",
"provider_api_format": "gemini:generate_content",
"model": "gpt-5"
}),
)
@@ -1062,7 +1062,7 @@ fn converts_gemini_cli_inline_data_to_openai_responses_output_image() {
}),
&json!({
"client_api_format": "openai:responses",
"provider_api_format": "gemini:cli",
"provider_api_format": "gemini:generate_content",
"model": "gpt-5"
}),
)
@@ -1084,7 +1084,7 @@ fn local_finalize_handles_openai_responses_compact_cross_format_sync_response()
report_kind: "openai_responses_compact_sync_finalize".to_string(),
report_context: Some(json!({
"client_api_format": "openai:responses:compact",
"provider_api_format": "gemini:cli",
"provider_api_format": "gemini:generate_content",
"model": "gpt-5",
"needs_conversion": true,
"has_envelope": false,
@@ -1140,7 +1140,7 @@ fn local_finalize_handles_openai_responses_compact_cross_format_function_call_re
report_kind: "openai_responses_compact_sync_finalize".to_string(),
report_context: Some(json!({
"client_api_format": "openai:responses:compact",
"provider_api_format": "gemini:cli",
"provider_api_format": "gemini:generate_content",
"model": "gpt-5",
"needs_conversion": true,
"has_envelope": false,
@@ -1311,7 +1311,7 @@ fn local_finalize_handles_openai_responses_cross_format_stream_response_from_gem
report_kind: "openai_responses_sync_finalize".to_string(),
report_context: Some(json!({
"client_api_format": "openai:responses",
"provider_api_format": "gemini:cli",
"provider_api_format": "gemini:generate_content",
"model": "gpt-5",
"mapped_model": "gemini-2.5-pro-upstream",
"needs_conversion": true,
@@ -1445,7 +1445,7 @@ fn local_finalize_handles_openai_chat_cross_format_sync_response_from_claude() {
report_kind: "openai_chat_sync_finalize".to_string(),
report_context: Some(json!({
"client_api_format": "openai:chat",
"provider_api_format": "claude:chat",
"provider_api_format": "claude:messages",
"model": "gpt-5",
"mapped_model": "claude-sonnet-4",
"needs_conversion": true,
@@ -1501,7 +1501,7 @@ fn local_finalize_handles_openai_chat_cross_format_sync_response_from_gemini() {
report_kind: "openai_chat_sync_finalize".to_string(),
report_context: Some(json!({
"client_api_format": "openai:chat",
"provider_api_format": "gemini:chat",
"provider_api_format": "gemini:generate_content",
"model": "gpt-5",
"mapped_model": "gemini-2.5-pro",
"needs_conversion": true,
@@ -1650,7 +1650,7 @@ fn local_finalize_handles_claude_chat_cross_format_sync_response_from_openai_cha
trace_id: "trace-claude-chat-xfmt-openai-sync-123".to_string(),
report_kind: "claude_chat_sync_finalize".to_string(),
report_context: Some(json!({
"client_api_format": "claude:chat",
"client_api_format": "claude:messages",
"provider_api_format": "openai:chat",
"model": "claude-sonnet-4-5",
"mapped_model": "gpt-5",
@@ -1690,7 +1690,7 @@ fn local_finalize_handles_claude_chat_cross_format_sync_response_from_openai_cha
route_class: Some("ai_public".to_string()),
route_family: Some("claude".to_string()),
route_kind: Some("chat".to_string()),
auth_endpoint_signature: Some("claude:chat".to_string()),
auth_endpoint_signature: Some("claude:messages".to_string()),
execution_runtime_candidate: true,
auth_context: None,
admin_principal: None,
@@ -1719,8 +1719,8 @@ fn local_finalize_handles_gemini_cli_cross_format_sync_response_from_claude_cli(
trace_id: "trace-gemini-cli-xfmt-claude-sync-123".to_string(),
report_kind: "gemini_cli_sync_finalize".to_string(),
report_context: Some(json!({
"client_api_format": "gemini:cli",
"provider_api_format": "claude:cli",
"client_api_format": "gemini:generate_content",
"provider_api_format": "claude:messages",
"model": "gemini-cli",
"mapped_model": "claude-code",
"needs_conversion": true,
@@ -1756,7 +1756,7 @@ fn local_finalize_handles_gemini_cli_cross_format_sync_response_from_claude_cli(
route_class: Some("ai_public".to_string()),
route_family: Some("gemini".to_string()),
route_kind: Some("cli".to_string()),
auth_endpoint_signature: Some("gemini:cli".to_string()),
auth_endpoint_signature: Some("gemini:generate_content".to_string()),
execution_runtime_candidate: true,
auth_context: None,
admin_principal: None,
@@ -388,7 +388,7 @@ fn resolve_request_transport_unsupported_reason(
}
return match provider_api_format.as_str() {
"openai:chat" => local_openai_chat_transport_unsupported_reason(transport),
"gemini:chat" | "gemini:cli" => local_gemini_transport_unsupported_reason_with_network(
"gemini:generate_content" => local_gemini_transport_unsupported_reason_with_network(
transport,
provider_api_format.as_str(),
),
@@ -472,7 +472,7 @@ mod tests {
config: None,
format_acceptance_config: Some(json!({
"enabled": true,
"accept_formats": ["claude:cli"]
"accept_formats": ["claude:messages"]
})),
proxy: None,
},
@@ -519,7 +519,7 @@ mod tests {
endpoint: GatewayProviderTransportEndpoint {
id: "endpoint-cc-1".to_string(),
provider_id: "provider-cc-1".to_string(),
api_format: "claude:cli".to_string(),
api_format: "claude:messages".to_string(),
api_family: Some("claude".to_string()),
endpoint_kind: Some("cli".to_string()),
is_active: true,
@@ -538,7 +538,7 @@ mod tests {
name: "CC-特价-0.4".to_string(),
auth_type: "api_key".to_string(),
is_active: true,
api_formats: Some(vec!["claude:cli".to_string()]),
api_formats: Some(vec!["claude:messages".to_string()]),
allowed_models: None,
capabilities: None,
rate_multipliers: None,
@@ -558,7 +558,7 @@ mod tests {
&sample_candidate(),
Some(&sample_transport()),
"openai:responses",
"claude:cli",
"claude:messages",
serde_json::Map::new(),
ExecutionStrategy::LocalCrossFormat,
ConversionMode::Bidirectional,
@@ -605,8 +605,8 @@ mod tests {
let metadata = build_local_execution_candidate_metadata_for_candidate(
&sample_candidate(),
Some(&sample_claude_code_transport_without_auth()),
"claude:cli",
"claude:cli",
"claude:messages",
"claude:messages",
serde_json::Map::new(),
);
@@ -151,7 +151,7 @@ fn planner_ranking_mode(mode: SchedulerSchedulingMode) -> SchedulerRankingMode {
}
fn normalize_api_format_alias(value: &str) -> String {
crate::ai_pipeline::normalize_legacy_openai_format_alias(value)
crate::ai_pipeline::normalize_api_format_alias(value)
}
fn api_format_matches(left: &str, right: &str) -> bool {
@@ -564,7 +564,7 @@ mod tests {
],
vec![
sample_endpoint_for_provider("provider-same", "endpoint-same", "openai:chat"),
sample_endpoint_for_provider("provider-cross", "endpoint-cross", "claude:chat"),
sample_endpoint_for_provider("provider-cross", "endpoint-cross", "claude:messages"),
],
vec![
sample_key_for_provider("provider-same", "key-same", ""),
@@ -586,7 +586,7 @@ mod tests {
"provider-cross",
"endpoint-cross",
"key-cross",
"claude:chat",
"claude:messages",
Some(0),
0,
),
@@ -618,7 +618,7 @@ mod tests {
],
vec![
sample_endpoint_for_provider("provider-same", "endpoint-same", "openai:chat"),
sample_endpoint_for_provider("provider-cross", "endpoint-cross", "claude:chat"),
sample_endpoint_for_provider("provider-cross", "endpoint-cross", "claude:messages"),
],
vec![
sample_key_for_provider("provider-same", "key-same", ""),
@@ -652,7 +652,7 @@ mod tests {
"provider-cross",
"endpoint-cross",
"key-cross",
"claude:chat",
"claude:messages",
Some(0),
0,
),
@@ -675,7 +675,7 @@ mod tests {
],
vec![
sample_endpoint_for_provider("provider-same", "endpoint-same", "openai:chat"),
sample_endpoint_for_provider("provider-cross", "endpoint-cross", "claude:chat"),
sample_endpoint_for_provider("provider-cross", "endpoint-cross", "claude:messages"),
],
vec![
sample_key_for_provider("provider-same", "key-same", ""),
@@ -697,7 +697,7 @@ mod tests {
"provider-cross",
"endpoint-cross",
"key-cross",
"claude:chat",
"claude:messages",
Some(0),
0,
),
@@ -728,7 +728,7 @@ mod tests {
],
vec![
sample_endpoint_for_provider("provider-same", "endpoint-same", "openai:chat"),
sample_endpoint_for_provider("provider-cross", "endpoint-cross", "claude:chat"),
sample_endpoint_for_provider("provider-cross", "endpoint-cross", "claude:messages"),
],
vec![
sample_key_for_provider("provider-same", "key-same", ""),
@@ -754,7 +754,7 @@ mod tests {
"provider-cross",
"endpoint-cross",
"key-cross",
"claude:chat",
"claude:messages",
Some(0),
0,
),
@@ -846,7 +846,11 @@ mod tests {
let provider_catalog = InMemoryProviderCatalogReadRepository::seed(
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",
"aaa-claude-chat",
"claude:messages",
),
sample_endpoint_for_provider(
"provider-shared",
"zzz-openai-responses",
@@ -858,7 +862,7 @@ mod tests {
"key-shared",
"",
true,
Some(json!(["claude:chat", "openai:responses"])),
Some(json!(["claude:messages", "openai:responses"])),
None,
)],
);
@@ -877,7 +881,7 @@ mod tests {
"provider-shared",
"aaa-claude-chat",
"key-shared",
"claude:chat",
"claude:messages",
Some(0),
0,
),
@@ -890,13 +894,13 @@ mod tests {
0,
),
],
"claude:cli",
"claude:messages",
None,
)
.await;
assert_eq!(ranked[0].endpoint_id, "zzz-openai-responses");
assert_eq!(ranked[1].endpoint_id, "aaa-claude-chat");
assert_eq!(ranked[0].endpoint_id, "aaa-claude-chat");
assert_eq!(ranked[1].endpoint_id, "zzz-openai-responses");
}
#[tokio::test]
@@ -1117,7 +1121,7 @@ mod tests {
sample_provider_with_options("provider-same", false, 10),
],
vec![
sample_endpoint_for_provider("provider-cross", "endpoint-cross", "claude:chat"),
sample_endpoint_for_provider("provider-cross", "endpoint-cross", "claude:messages"),
sample_endpoint_for_provider("provider-same", "endpoint-same", "openai:chat"),
],
vec![
@@ -1126,7 +1130,7 @@ mod tests {
"key-cross",
"",
true,
Some(json!(["claude:chat"])),
Some(json!(["claude:messages"])),
None,
),
sample_key_for_provider_with_options(
@@ -1154,7 +1158,7 @@ mod tests {
"provider-cross",
"endpoint-cross",
"key-cross",
"claude:chat",
"claude:messages",
Some(0),
0,
),
@@ -1188,14 +1192,18 @@ mod tests {
vec![sample_provider_with_options("provider-shared", false, 0)],
vec![
sample_endpoint_for_provider("provider-shared", "endpoint-exact", "openai:chat"),
sample_endpoint_for_provider("provider-shared", "endpoint-cross", "claude:chat"),
sample_endpoint_for_provider(
"provider-shared",
"endpoint-cross",
"claude:messages",
),
],
vec![sample_key_for_provider_with_options(
"provider-shared",
"key-shared",
"",
true,
Some(json!(["openai:chat", "claude:chat"])),
Some(json!(["openai:chat", "claude:messages"])),
None,
)],
);
@@ -1222,7 +1230,7 @@ mod tests {
"provider-shared",
"endpoint-cross",
"key-shared",
"claude:chat",
"claude:messages",
Some(0),
0,
),
@@ -1245,7 +1253,7 @@ mod tests {
#[tokio::test]
async fn realtime_gate_allows_cross_format_candidates_when_endpoint_acceptance_is_enabled() {
let mut endpoint_cross =
sample_endpoint_for_provider("provider-cross", "endpoint-cross", "claude:chat");
sample_endpoint_for_provider("provider-cross", "endpoint-cross", "claude:messages");
endpoint_cross.format_acceptance_config = Some(json!({
"enabled": true,
"accept_formats": ["openai:chat"],
@@ -1266,7 +1274,7 @@ mod tests {
"key-cross",
"",
true,
Some(json!(["claude:chat"])),
Some(json!(["claude:messages"])),
None,
),
sample_key_for_provider_with_options(
@@ -1294,7 +1302,7 @@ mod tests {
"provider-cross",
"endpoint-cross",
"key-cross",
"claude:chat",
"claude:messages",
Some(0),
0,
),
@@ -346,7 +346,7 @@ fn transport_key_supports_api_format(
}
fn normalize_api_format_alias(value: &str) -> String {
crate::ai_pipeline::normalize_legacy_openai_format_alias(value)
crate::ai_pipeline::normalize_api_format_alias(value)
}
fn api_format_matches(left: &str, right: &str) -> bool {
@@ -132,12 +132,15 @@ mod tests {
use serde_json::json;
#[test]
fn forces_streaming_for_codex_openai_responses_and_alias() {
fn forces_streaming_for_codex_openai_responses() {
assert!(force_upstream_streaming_for_provider(
"codex",
"openai:responses"
));
assert!(force_upstream_streaming_for_provider("codex", "openai:cli"));
assert!(!force_upstream_streaming_for_provider(
"codex",
"openai:responses:compact"
));
}
#[test]
@@ -148,11 +151,11 @@ mod tests {
));
assert!(!force_upstream_streaming_for_provider(
"codex",
"openai:compact"
"openai:responses:compact"
));
assert!(!force_upstream_streaming_for_provider(
"openai",
"openai:cli"
"openai:responses"
));
}
@@ -36,20 +36,15 @@ pub(crate) fn resolve_same_format_provider_transport_unsupported_reason_for_trac
transport: &GatewayProviderTransportSnapshot,
provider_api_format: &str,
) -> Option<&'static str> {
let provider_api_format = match crate::ai_pipeline::normalize_legacy_openai_format_alias(
provider_api_format,
)
.as_str()
{
"openai:chat" => "openai:chat",
"openai:responses" => "openai:responses",
"openai:responses:compact" => "openai:responses:compact",
"claude:chat" => "claude:chat",
"claude:cli" => "claude:cli",
"gemini:chat" => "gemini:chat",
"gemini:cli" => "gemini:cli",
_ => return Some("transport_api_format_unsupported"),
};
let provider_api_format =
match crate::ai_pipeline::normalize_api_format_alias(provider_api_format).as_str() {
"openai:chat" => "openai:chat",
"openai:responses" => "openai:responses",
"openai:responses:compact" => "openai:responses:compact",
"claude:messages" => "claude:messages",
"gemini:generate_content" => "gemini:generate_content",
_ => return Some("transport_api_format_unsupported"),
};
let behavior = policy::classify_same_format_provider_request_behavior(
transport,
crate::ai_pipeline::planner::spec_metadata::LocalExecutionSurfaceSpecMetadata {
@@ -50,9 +50,9 @@ pub(super) fn classify_same_format_provider_request_behavior(
let report_kind = if is_kiro && !spec_metadata.require_streaming {
"claude_cli_sync_finalize"
} else if is_antigravity && !spec_metadata.require_streaming {
match spec_metadata.api_format {
"gemini:chat" => "gemini_chat_sync_finalize",
"gemini:cli" => "gemini_cli_sync_finalize",
match default_report_kind {
"gemini_chat_sync_success" => "gemini_chat_sync_finalize",
"gemini_cli_sync_success" => "gemini_cli_sync_finalize",
_ => default_report_kind,
}
} else {
@@ -32,7 +32,7 @@ use crate::{AppState, GatewayError};
pub(super) use crate::ai_pipeline::planner::candidate_materialization::LocalExecutionCandidateAttempt as LocalGeminiFilesCandidateAttempt;
pub(super) use crate::ai_pipeline::planner::decision_input::LocalAuthenticatedDecisionInput as LocalGeminiFilesDecisionInput;
pub(super) const GEMINI_FILES_CANDIDATE_API_FORMAT: &str = "gemini:chat";
pub(super) const GEMINI_FILES_CANDIDATE_API_FORMAT: &str = "gemini:files";
pub(super) const GEMINI_FILES_CLIENT_API_FORMAT: &str = "gemini:files";
pub(super) const GEMINI_FILES_REQUIRED_CAPABILITY: &str = "gemini_files";
@@ -63,7 +63,7 @@ pub(crate) async fn resolve_local_standard_candidate_payload_parts(
.provider_type
.trim()
.eq_ignore_ascii_case("kiro")
&& provider_api_format.eq_ignore_ascii_case("claude:cli");
&& provider_api_format.eq_ignore_ascii_case("claude:messages");
let Some(conversion_kind) = crate::ai_pipeline::conversion::request_conversion_kind(
spec_metadata.api_format,
provider_api_format,
@@ -120,7 +120,7 @@ mod tests {
let converted = build_standard_request_body(
&request,
"claude:chat",
"claude:messages",
"gpt-5",
"openai",
"openai:chat",
@@ -154,10 +154,10 @@ mod tests {
let converted = build_standard_request_body(
&request,
"gemini:chat",
"gemini:generate_content",
"claude-sonnet-4-5",
"anthropic",
"claude:chat",
"claude:messages",
"/v1beta/models/gemini-2.5-pro:generateContent",
false,
None,
@@ -190,10 +190,10 @@ mod tests {
let converted = build_standard_request_body(
&request,
"claude:cli",
"claude:messages",
"gemini-2.5-pro",
"google",
"gemini:cli",
"gemini:generate_content",
"/v1/messages",
false,
None,
@@ -223,7 +223,7 @@ mod tests {
let converted = build_standard_request_body(
&request,
"claude:cli",
"claude:messages",
"gpt-5",
"openai",
"openai:responses",
@@ -258,7 +258,7 @@ mod tests {
let converted = build_standard_request_body(
&request,
"claude:cli",
"claude:messages",
"gpt-5",
"codex",
"openai:responses",
@@ -197,7 +197,7 @@ fn strips_metadata_for_codex_openai_responses_requests() {
let provider_request_body = build_cross_format_openai_responses_request_body(
&body_json,
"gpt-5-upstream",
"claude:cli",
"claude:messages",
"openai:responses",
true,
"codex",
@@ -229,7 +229,7 @@ fn applies_codex_defaults_unless_body_rules_handle_the_field() {
let provider_request_body = build_cross_format_openai_responses_request_body(
&body_json,
"gpt-5-upstream",
"claude:cli",
"claude:messages",
"openai:responses",
true,
"codex",
@@ -256,7 +256,7 @@ fn injects_codex_prompt_cache_key_for_openai_responses_cross_format_requests() {
let provider_request_body = build_cross_format_openai_responses_request_body(
&body_json,
"gpt-5-upstream",
"claude:cli",
"claude:messages",
"openai:responses",
true,
"codex",
@@ -265,7 +265,7 @@ pub(crate) async fn resolve_local_openai_chat_candidate_payload_parts(
.provider_type
.trim()
.eq_ignore_ascii_case("kiro")
&& provider_api_format.eq_ignore_ascii_case("claude:cli");
&& provider_api_format.eq_ignore_ascii_case("claude:messages");
let oauth_context = OauthPreparationContext {
trace_id,
api_format: provider_api_format.as_str(),
@@ -145,13 +145,13 @@ mod tests {
"codex",
"openai:responses"
));
assert!(openai_chat_sync_upstream_is_stream_for_candidate(
assert!(!openai_chat_sync_upstream_is_stream_for_candidate(
"codex",
"openai:cli"
"openai:responses:compact"
));
assert!(!openai_chat_sync_upstream_is_stream_for_candidate(
"openai",
"openai:cli"
"openai:responses"
));
assert!(!openai_chat_sync_upstream_is_stream_for_candidate(
"codex",
@@ -513,9 +513,9 @@ mod tests {
provider_request_method: None,
auth_header: Some("x-api-key".to_string()),
auth_value: Some("sk-upstream-claude".to_string()),
provider_api_format: Some("claude:chat".to_string()),
provider_api_format: Some("claude:messages".to_string()),
client_api_format: Some("openai:chat".to_string()),
provider_contract: Some("claude:chat".to_string()),
provider_contract: Some("claude:messages".to_string()),
client_contract: Some("openai:chat".to_string()),
model_name: Some("claude-sonnet-4-5".to_string()),
mapped_model: Some("claude-sonnet-4-5".to_string()),
@@ -500,9 +500,9 @@ mod tests {
provider_request_method: None,
auth_header: Some("x-api-key".to_string()),
auth_value: Some("sk-upstream-claude".to_string()),
provider_api_format: Some("claude:chat".to_string()),
provider_api_format: Some("claude:messages".to_string()),
client_api_format: Some("openai:chat".to_string()),
provider_contract: Some("claude:chat".to_string()),
provider_contract: Some("claude:messages".to_string()),
client_contract: Some("openai:chat".to_string()),
model_name: Some("claude-sonnet-4-5".to_string()),
mapped_model: Some("claude-sonnet-4-5".to_string()),
@@ -94,7 +94,7 @@ pub(crate) async fn resolve_local_openai_responses_candidate_payload_parts(
.provider_type
.trim()
.eq_ignore_ascii_case("kiro")
&& provider_api_format.eq_ignore_ascii_case("claude:cli");
&& provider_api_format.eq_ignore_ascii_case("claude:messages");
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);
@@ -104,7 +104,7 @@ pub(crate) async fn resolve_local_openai_responses_candidate_payload_parts(
local_standard_transport_unsupported_reason_with_network(transport, provider_api_format)
} else {
match conversion_kind {
Some(_) if is_antigravity && provider_api_format == "gemini:cli" => None,
Some(_) if is_antigravity && provider_api_format == "gemini:generate_content" => None,
Some(kind) => {
crate::ai_pipeline::conversion::request_conversion_transport_unsupported_reason(
transport, kind,
@@ -162,10 +162,9 @@ pub(crate) async fn resolve_local_openai_responses_candidate_payload_parts(
let direct_auth = if kiro_auth.is_some() {
None
} else if same_format {
match crate::ai_pipeline::normalize_legacy_openai_format_alias(provider_api_format).as_str()
{
"gemini:cli" => resolve_local_gemini_auth(transport),
"claude:cli" => resolve_local_standard_auth(transport),
match crate::ai_pipeline::normalize_api_format_alias(provider_api_format).as_str() {
"gemini:generate_content" => resolve_local_gemini_auth(transport),
"claude:messages" => resolve_local_standard_auth(transport),
"openai:responses" | "openai:responses:compact" => {
resolve_local_openai_bearer_auth(transport)
}
@@ -536,7 +535,7 @@ pub(crate) async fn resolve_local_openai_responses_candidate_payload_parts(
}
fn api_format_alias_matches(left: &str, right: &str) -> bool {
crate::ai_pipeline::legacy_openai_format_alias_matches(left, right)
crate::ai_pipeline::api_format_alias_matches(left, right)
}
#[allow(clippy::too_many_arguments)]
@@ -342,7 +342,7 @@ pub(crate) async fn materialize_local_openai_responses_candidate_attempts(
}
fn api_format_alias_matches(left: &str, right: &str) -> bool {
crate::ai_pipeline::legacy_openai_format_alias_matches(left, right)
crate::ai_pipeline::api_format_alias_matches(left, right)
}
pub(crate) async fn mark_skipped_local_openai_responses_candidate(
state: &AppState,
@@ -604,8 +604,9 @@ mod tests {
}]
});
let diagnostic = request_body_build_failure_extra_data(&body, "openai:chat", "claude:chat")
.expect("diagnostic");
let diagnostic =
request_body_build_failure_extra_data(&body, "openai:chat", "claude:messages")
.expect("diagnostic");
assert_eq!(
diagnostic["request_body_build_error"]["path"],
@@ -617,7 +618,7 @@ mod tests {
);
assert_eq!(
diagnostic["failure_diagnostic"]["source"],
"openai:chat_to_claude:chat"
"openai:chat_to_claude:messages"
);
assert!(diagnostic["request_body_build_error"]["message"]
.as_str()
@@ -635,8 +636,9 @@ mod tests {
}]
});
let diagnostic = request_body_build_failure_extra_data(&body, "openai:chat", "claude:chat")
.expect("diagnostic");
let diagnostic =
request_body_build_failure_extra_data(&body, "openai:chat", "claude:messages")
.expect("diagnostic");
assert_eq!(
diagnostic["request_body_build_error"]["path"],
@@ -657,8 +659,9 @@ mod tests {
}]
});
let diagnostic = request_body_build_failure_extra_data(&body, "openai:chat", "gemini:chat")
.expect("diagnostic");
let diagnostic =
request_body_build_failure_extra_data(&body, "openai:chat", "gemini:generate_content")
.expect("diagnostic");
assert_eq!(
diagnostic["request_body_build_error"]["path"],
@@ -681,7 +684,7 @@ mod tests {
});
let diagnostic =
request_body_build_failure_extra_data(&body, "openai:responses", "claude:chat")
request_body_build_failure_extra_data(&body, "openai:responses", "claude:messages")
.expect("diagnostic");
assert_eq!(
@@ -698,9 +701,12 @@ mod tests {
"tool_choice": { "type": "function" }
});
let diagnostic =
request_body_build_failure_extra_data(&body, "openai:responses", "gemini:chat")
.expect("diagnostic");
let diagnostic = request_body_build_failure_extra_data(
&body,
"openai:responses",
"gemini:generate_content",
)
.expect("diagnostic");
assert_eq!(
diagnostic["request_body_build_error"]["path"],
@@ -2,7 +2,8 @@ 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_responses_stream_sync_response,
aggregate_standard_chat_stream_sync_response, aggregate_standard_cli_stream_sync_response,
apply_codex_openai_responses_special_body_edits, apply_codex_openai_responses_special_headers,
api_format_alias_matches, 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_responses_request_body, build_generated_tool_call_id,
@@ -27,17 +28,17 @@ pub(crate) use aether_ai_pipeline::api::{
force_upstream_streaming_for_provider, generic_decision_missing_exact_provider_request,
implicit_sync_finalize_report_kind, is_core_error_finalize_kind, is_matching_stream_request,
is_openai_responses_family_format, is_openai_responses_format, kiro_crc32,
legacy_openai_format_alias_matches, map_claude_stop_reason,
map_openai_reasoning_effort_to_claude_output, map_openai_reasoning_effort_to_gemini_budget,
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_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,
maybe_build_standard_sync_finalize_product_from_normalized_payload, normalize_api_format_alias,
normalize_claude_request_to_openai_chat_request,
normalize_gemini_request_to_openai_chat_request, normalize_legacy_openai_format_alias,
normalize_gemini_request_to_openai_chat_request,
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,