Merge remote-tracking branch 'origin/aether-rust-pioneer' into codex/async-cleanup-records

# Conflicts:
#	apps/aether-gateway/src/maintenance/mod.rs
#	apps/aether-gateway/src/maintenance/runtime/runners.rs
This commit is contained in:
fawney19
2026-05-10 00:28:39 +08:00
315 changed files with 19750 additions and 3183 deletions

View File

@@ -301,6 +301,9 @@ pub fn build_admin_monitoring_trace_request_payload_response_with_key_accounts(
.collect::<Vec<_>>();
Json(json!({
"request_id": trace.request_id,
"request_path": admin_monitoring_trace_request_path(usage),
"request_query_string": admin_monitoring_trace_request_query_string(usage),
"request_path_and_query": admin_monitoring_trace_request_path_and_query(usage),
"total_candidates": trace.total_candidates,
"final_status": trace.final_status,
"total_latency_ms": trace.total_latency_ms,
@@ -470,6 +473,33 @@ fn build_admin_monitoring_trace_candidate_extra_data(
.entry("first_byte_time_ms".to_string())
.or_insert_with(|| json!(first_byte_time_ms));
}
if let Some(request_path) = admin_monitoring_usage_request_path(usage) {
extra_object
.entry("request_path".to_string())
.or_insert_with(|| json!(request_path));
}
if let Some(request_query_string) = admin_monitoring_usage_request_query_string(usage) {
extra_object
.entry("request_query_string".to_string())
.or_insert_with(|| json!(request_query_string));
}
if let Some(request_path_and_query) = admin_monitoring_usage_request_path_and_query(usage) {
extra_object
.entry("request_path_and_query".to_string())
.or_insert_with(|| json!(request_path_and_query));
}
if admin_monitoring_usage_is_error_node(usage) {
if let Some(response) = admin_monitoring_trace_response_data(
"upstream_response",
usage.status_code,
usage.response_headers.as_ref(),
usage.response_body.as_ref(),
usage.response_body_ref.as_deref(),
usage.response_body_state,
) {
extra_object.insert("upstream_response".to_string(), response);
}
}
if let Some(proxy_value) = extra_object.get_mut("proxy") {
if let Some(proxy_object) = proxy_value.as_object_mut() {
@@ -497,6 +527,96 @@ fn build_admin_monitoring_trace_candidate_extra_data(
}
}
fn admin_monitoring_trace_response_data(
source: &str,
status_code: Option<u16>,
headers: Option<&Value>,
body: Option<&Value>,
body_ref: Option<&str>,
body_state: Option<aether_data_contracts::repository::usage::UsageBodyCaptureState>,
) -> Option<Value> {
if status_code.is_none()
&& headers.is_none()
&& body.is_none()
&& body_ref.is_none()
&& body_state.is_none()
{
return None;
}
Some(json!({
"source": source,
"status_code": status_code,
"headers": headers.cloned().unwrap_or(Value::Null),
"body": body.cloned().unwrap_or(Value::Null),
"body_ref": body_ref,
"body_state": body_state.map(|state| state.as_str()),
}))
}
fn admin_monitoring_usage_is_error_node(usage: &StoredRequestUsageAudit) -> bool {
!usage.status.eq_ignore_ascii_case("completed")
|| usage
.status_code
.is_some_and(|status| !(200..300).contains(&status))
}
fn admin_monitoring_trace_request_path(usage: Option<&StoredRequestUsageAudit>) -> Option<String> {
usage.and_then(admin_monitoring_usage_request_path)
}
fn admin_monitoring_trace_request_query_string(
usage: Option<&StoredRequestUsageAudit>,
) -> Option<String> {
usage.and_then(admin_monitoring_usage_request_query_string)
}
fn admin_monitoring_trace_request_path_and_query(
usage: Option<&StoredRequestUsageAudit>,
) -> Option<String> {
usage.and_then(admin_monitoring_usage_request_path_and_query)
}
fn admin_monitoring_usage_request_path(usage: &StoredRequestUsageAudit) -> Option<String> {
admin_monitoring_usage_metadata_string(usage, "request_path")
}
fn admin_monitoring_usage_request_query_string(usage: &StoredRequestUsageAudit) -> Option<String> {
admin_monitoring_usage_metadata_string(usage, "request_query_string")
.map(|value| value.trim_start_matches('?').to_string())
.filter(|value| !value.is_empty())
}
fn admin_monitoring_usage_request_path_and_query(
usage: &StoredRequestUsageAudit,
) -> Option<String> {
admin_monitoring_usage_metadata_string(usage, "request_path_and_query").or_else(|| {
let path = admin_monitoring_usage_metadata_string(usage, "request_path")?;
let query = admin_monitoring_usage_metadata_string(usage, "request_query_string")
.map(|value| value.trim_start_matches('?').to_string())
.filter(|value| !value.is_empty());
Some(match query {
Some(query) if !path.contains('?') => format!("{path}?{query}"),
_ => path,
})
})
}
fn admin_monitoring_usage_metadata_string(
usage: &StoredRequestUsageAudit,
key: &str,
) -> Option<String> {
usage
.request_metadata
.as_ref()
.and_then(Value::as_object)
.and_then(|metadata| metadata.get(key))
.and_then(Value::as_str)
.map(str::trim)
.filter(|value| !value.is_empty())
.map(ToOwned::to_owned)
}
fn json_string_field(object: &serde_json::Map<String, Value>, key: &str) -> Option<String> {
object
.get(key)

View File

@@ -1,4 +1,5 @@
use crate::observability::stats::{aggregate_usage_stats, parse_bounded_u32, round_to};
use aether_ai_formats::api::request_path_implies_stream_request;
use aether_billing::{
normalize_input_tokens_for_billing, normalize_total_input_context_for_cache_hit_rate,
};
@@ -227,7 +228,9 @@ pub fn admin_usage_matches_api_format(
}
pub fn admin_usage_is_failed(item: &StoredRequestUsageAudit) -> bool {
let has_failure_signal = item.status_code.is_some_and(|value| value >= 400)
let has_failure_signal = item
.status_code
.is_some_and(|value| !(200..300).contains(&value))
|| item
.error_message
.as_deref()
@@ -256,7 +259,9 @@ pub fn admin_usage_matches_status(item: &StoredRequestUsageAudit, status: Option
"stream" => item.is_stream,
"standard" => !item.is_stream,
"error" => {
item.status_code.is_some_and(|value| value >= 400) || item.error_message.is_some()
item.status_code
.is_some_and(|value| !(200..300).contains(&value))
|| item.error_message.is_some()
}
"pending" | "streaming" | "completed" | "cancelled" => item.status == status,
"failed" => admin_usage_is_failed(item),
@@ -955,12 +960,26 @@ fn admin_usage_infer_upstream_stream_from_captured_bodies(
}
}
fn admin_usage_request_path_implies_client_stream(item: &StoredRequestUsageAudit) -> bool {
let Some(metadata) = item.request_metadata.as_ref().and_then(Value::as_object) else {
return false;
};
["request_path", "request_path_and_query"]
.into_iter()
.filter_map(|field| metadata.get(field).and_then(Value::as_str))
.any(request_path_implies_stream_request)
}
pub fn admin_usage_client_is_stream(item: &StoredRequestUsageAudit) -> bool {
item.request_metadata
.as_ref()
.and_then(Value::as_object)
.and_then(|metadata| metadata.get("client_requested_stream"))
.and_then(Value::as_bool)
admin_usage_request_path_implies_client_stream(item)
.then_some(true)
.or_else(|| {
item.request_metadata
.as_ref()
.and_then(Value::as_object)
.and_then(|metadata| metadata.get("client_requested_stream"))
.and_then(Value::as_bool)
})
.or_else(|| admin_usage_request_body_stream_flag(item))
.or_else(|| admin_usage_headers_stream_flag(item.client_response_headers.as_ref()))
.or_else(|| admin_usage_request_body_implies_default_non_stream(item).then_some(false))
@@ -1665,7 +1684,9 @@ pub fn admin_usage_is_success(item: &StoredRequestUsageAudit) -> bool {
matches!(
item.status.as_str(),
"completed" | "success" | "ok" | "billed" | "settled"
) && item.status_code.is_none_or(|code| code < 400)
) && item
.status_code
.is_none_or(|code| (200..300).contains(&code))
}
pub fn admin_usage_matches_optional_id(value: Option<&str>, expected: Option<&str>) -> bool {
@@ -2258,10 +2279,10 @@ mod tests {
use super::{
admin_usage_active_request_json, admin_usage_client_is_stream, admin_usage_has_body_value,
admin_usage_has_fallback, admin_usage_is_failed, admin_usage_matches_search,
admin_usage_matches_status, admin_usage_matches_username, admin_usage_record_json,
admin_usage_resolve_request_capture_body, admin_usage_upstream_is_stream,
build_admin_usage_detail_payload,
admin_usage_has_fallback, admin_usage_is_failed, admin_usage_is_success,
admin_usage_matches_search, admin_usage_matches_status, admin_usage_matches_username,
admin_usage_record_json, admin_usage_resolve_request_capture_body,
admin_usage_upstream_is_stream, build_admin_usage_detail_payload,
};
use aether_data_contracts::repository::usage::{StoredRequestUsageAudit, UsageBodyField};
@@ -2349,6 +2370,33 @@ mod tests {
assert_eq!(record["client_is_stream"], false);
}
#[test]
fn client_requested_stream_uses_stream_generate_content_path_over_stale_metadata_flag() {
let item = StoredRequestUsageAudit {
is_stream: true,
request_metadata: Some(json!({
"client_requested_stream": false,
"request_path": "/v1beta/models/gemini-3.1-flash-image-preview:streamGenerateContent",
"request_path_and_query": "/v1beta/models/gemini-3.1-flash-image-preview:streamGenerateContent?alt=sse"
})),
..sample_usage("completed", Some(200), None)
};
assert!(admin_usage_client_is_stream(&item));
let record = admin_usage_record_json(
&item,
&BTreeMap::new(),
&BTreeMap::new(),
false,
false,
None,
);
assert_eq!(record["upstream_is_stream"], true);
assert_eq!(record["client_requested_stream"], true);
assert_eq!(record["client_is_stream"], true);
}
#[test]
fn client_requested_stream_falls_back_to_request_body_stream_flag() {
let item = StoredRequestUsageAudit {
@@ -2528,6 +2576,30 @@ mod tests {
assert!(admin_usage_matches_status(&item, Some("failed")));
}
#[test]
fn redirect_status_is_not_admin_usage_success() {
let item = sample_usage("completed", Some(302), None);
assert!(!admin_usage_is_success(&item));
assert!(!admin_usage_is_failed(&item));
assert!(admin_usage_matches_status(&item, Some("error")));
assert!(admin_usage_matches_status(&item, Some("completed")));
}
#[test]
fn failed_redirect_status_counts_as_admin_usage_failed() {
let item = sample_usage(
"failed",
Some(302),
Some("execution runtime stream returned non-success status 302"),
);
assert!(admin_usage_is_failed(&item));
assert!(admin_usage_matches_status(&item, Some("failed")));
assert!(admin_usage_matches_status(&item, Some("error")));
assert!(!admin_usage_is_success(&item));
}
#[test]
fn active_status_with_failure_signal_counts_as_failed() {
let item = sample_usage("pending", Some(503), Some("upstream failed"));

View File

@@ -1,6 +1,9 @@
use aether_data::repository::{
auth_modules::{StoredLdapModuleConfig, StoredOAuthProviderModuleConfig},
proxy_nodes::{StoredProxyNode, StoredProxyNodeEvent},
proxy_nodes::{
ProxyNodeMetricsStep, StoredProxyFleetMetricsBucket, StoredProxyNode, StoredProxyNodeEvent,
StoredProxyNodeMetricsBucket,
},
system::StoredSystemConfigEntry,
wallet::StoredWalletSnapshot,
};
@@ -1255,6 +1258,9 @@ pub fn admin_system_config_default_value(key: &str) -> Option<serde_json::Value>
"cleanup_batch_size" => Some(json!(1000)),
"request_candidates_retention_days" => Some(json!(30)),
"request_candidates_cleanup_batch_size" => Some(json!(5000)),
"proxy_node_metrics_1m_retention_days" => Some(json!(30)),
"proxy_node_metrics_1h_retention_days" => Some(json!(180)),
"proxy_node_metrics_cleanup_batch_size" => Some(json!(5000)),
"enable_provider_checkin" => Some(json!(true)),
"provider_checkin_time" => Some(json!("01:05")),
"provider_priority_mode" => Some(json!("provider")),
@@ -1832,11 +1838,13 @@ pub fn build_admin_proxy_node_event_payload(event: &StoredProxyNodeEvent) -> ser
"id": event.id,
"event_type": event.event_type,
"detail": event.detail,
"event_metadata": event.event_metadata,
"created_at": event.created_at_unix_ms.and_then(unix_secs_to_rfc3339),
})
}
pub fn admin_proxy_node_event_node_id_from_path(request_path: &str) -> Option<&str> {
let request_path = request_path.trim_end_matches('/');
let node_id = request_path.strip_prefix("/api/admin/proxy-nodes/")?;
let node_id = node_id.strip_suffix("/events")?;
if node_id.is_empty() || node_id.contains('/') {
@@ -1846,6 +1854,219 @@ pub fn admin_proxy_node_event_node_id_from_path(request_path: &str) -> Option<&s
}
}
pub fn admin_proxy_node_metrics_node_id_from_path(request_path: &str) -> Option<&str> {
let request_path = request_path.trim_end_matches('/');
let node_id = request_path.strip_prefix("/api/admin/proxy-nodes/")?;
let node_id = node_id.strip_suffix("/metrics")?;
if node_id.is_empty() || node_id.contains('/') {
None
} else {
Some(node_id)
}
}
pub fn build_admin_proxy_node_metrics_payload_response(
step: ProxyNodeMetricsStep,
from_unix_secs: u64,
to_unix_secs: u64,
items: Vec<StoredProxyNodeMetricsBucket>,
) -> Response<Body> {
let summary = summarize_proxy_node_metric_buckets(items.iter().map(|item| {
(
item.samples,
item.uptime_samples,
item.active_connections_sum,
item.active_connections_max,
item.heartbeat_rtt_ms_sum,
item.heartbeat_rtt_ms_max,
item.connect_errors_delta,
item.disconnects_delta,
item.error_events_delta,
item.ws_in_bytes_delta,
item.ws_out_bytes_delta,
item.ws_in_frames_delta,
item.ws_out_frames_delta,
)
}));
let items = items
.into_iter()
.map(build_admin_proxy_node_metrics_bucket_payload)
.collect::<Vec<_>>();
Json(json!({
"step": step.as_api_value(),
"from": from_unix_secs,
"to": to_unix_secs,
"items": items,
"summary": summary,
}))
.into_response()
}
pub fn build_admin_proxy_fleet_metrics_payload_response(
step: ProxyNodeMetricsStep,
from_unix_secs: u64,
to_unix_secs: u64,
items: Vec<StoredProxyFleetMetricsBucket>,
) -> Response<Body> {
let summary = summarize_proxy_node_metric_buckets(items.iter().map(|item| {
(
item.samples,
item.uptime_samples,
item.active_connections_sum,
item.active_connections_max,
item.heartbeat_rtt_ms_sum,
item.heartbeat_rtt_ms_max,
item.connect_errors_delta,
item.disconnects_delta,
item.error_events_delta,
item.ws_in_bytes_delta,
item.ws_out_bytes_delta,
item.ws_in_frames_delta,
item.ws_out_frames_delta,
)
}));
let items = items
.into_iter()
.map(build_admin_proxy_fleet_metrics_bucket_payload)
.collect::<Vec<_>>();
Json(json!({
"step": step.as_api_value(),
"from": from_unix_secs,
"to": to_unix_secs,
"items": items,
"summary": summary,
}))
.into_response()
}
fn build_admin_proxy_node_metrics_bucket_payload(
item: StoredProxyNodeMetricsBucket,
) -> serde_json::Value {
json!({
"node_id": item.node_id,
"bucket_start_unix_secs": item.bucket_start_unix_secs,
"bucket_start": unix_secs_to_rfc3339(item.bucket_start_unix_secs),
"samples": item.samples,
"uptime_samples": item.uptime_samples,
"uptime_ratio": ratio(item.uptime_samples, item.samples),
"active_connections_sum": item.active_connections_sum,
"active_connections_max": item.active_connections_max,
"active_connections_avg": ratio(item.active_connections_sum, item.samples),
"heartbeat_rtt_ms_sum": item.heartbeat_rtt_ms_sum,
"heartbeat_rtt_ms_max": item.heartbeat_rtt_ms_max,
"heartbeat_rtt_ms_avg": ratio(item.heartbeat_rtt_ms_sum, item.samples),
"connect_errors_delta": item.connect_errors_delta,
"disconnects_delta": item.disconnects_delta,
"error_events_delta": item.error_events_delta,
"ws_in_bytes_delta": item.ws_in_bytes_delta,
"ws_out_bytes_delta": item.ws_out_bytes_delta,
"ws_in_frames_delta": item.ws_in_frames_delta,
"ws_out_frames_delta": item.ws_out_frames_delta,
})
}
fn build_admin_proxy_fleet_metrics_bucket_payload(
item: StoredProxyFleetMetricsBucket,
) -> serde_json::Value {
json!({
"bucket_start_unix_secs": item.bucket_start_unix_secs,
"bucket_start": unix_secs_to_rfc3339(item.bucket_start_unix_secs),
"samples": item.samples,
"uptime_samples": item.uptime_samples,
"uptime_ratio": ratio(item.uptime_samples, item.samples),
"active_connections_sum": item.active_connections_sum,
"active_connections_max": item.active_connections_max,
"active_connections_avg": ratio(item.active_connections_sum, item.samples),
"heartbeat_rtt_ms_sum": item.heartbeat_rtt_ms_sum,
"heartbeat_rtt_ms_max": item.heartbeat_rtt_ms_max,
"heartbeat_rtt_ms_avg": ratio(item.heartbeat_rtt_ms_sum, item.samples),
"connect_errors_delta": item.connect_errors_delta,
"disconnects_delta": item.disconnects_delta,
"error_events_delta": item.error_events_delta,
"ws_in_bytes_delta": item.ws_in_bytes_delta,
"ws_out_bytes_delta": item.ws_out_bytes_delta,
"ws_in_frames_delta": item.ws_in_frames_delta,
"ws_out_frames_delta": item.ws_out_frames_delta,
})
}
fn summarize_proxy_node_metric_buckets<I>(items: I) -> serde_json::Value
where
I: IntoIterator<
Item = (
i64,
i64,
i64,
i64,
i64,
i64,
i64,
i64,
i64,
i64,
i64,
i64,
i64,
),
>,
{
let mut samples = 0;
let mut uptime_samples = 0;
let mut active_connections_sum = 0;
let mut active_connections_max = 0;
let mut heartbeat_rtt_ms_sum = 0;
let mut heartbeat_rtt_ms_max = 0;
let mut connect_errors_delta = 0;
let mut disconnects_delta = 0;
let mut error_events_delta = 0;
let mut ws_in_bytes_delta = 0;
let mut ws_out_bytes_delta = 0;
let mut ws_in_frames_delta = 0;
let mut ws_out_frames_delta = 0;
for item in items {
samples += item.0;
uptime_samples += item.1;
active_connections_sum += item.2;
active_connections_max = active_connections_max.max(item.3);
heartbeat_rtt_ms_sum += item.4;
heartbeat_rtt_ms_max = heartbeat_rtt_ms_max.max(item.5);
connect_errors_delta += item.6;
disconnects_delta += item.7;
error_events_delta += item.8;
ws_in_bytes_delta += item.9;
ws_out_bytes_delta += item.10;
ws_in_frames_delta += item.11;
ws_out_frames_delta += item.12;
}
json!({
"samples": samples,
"uptime_samples": uptime_samples,
"uptime_ratio": ratio(uptime_samples, samples),
"active_connections_sum": active_connections_sum,
"active_connections_max": active_connections_max,
"active_connections_avg": ratio(active_connections_sum, samples),
"heartbeat_rtt_ms_sum": heartbeat_rtt_ms_sum,
"heartbeat_rtt_ms_max": heartbeat_rtt_ms_max,
"heartbeat_rtt_ms_avg": ratio(heartbeat_rtt_ms_sum, samples),
"connect_errors_delta": connect_errors_delta,
"disconnects_delta": disconnects_delta,
"error_events_delta": error_events_delta,
"ws_in_bytes_delta": ws_in_bytes_delta,
"ws_out_bytes_delta": ws_out_bytes_delta,
"ws_in_frames_delta": ws_in_frames_delta,
"ws_out_frames_delta": ws_out_frames_delta,
})
}
fn ratio(numerator: i64, denominator: i64) -> Option<f64> {
if denominator <= 0 {
return None;
}
Some(numerator as f64 / denominator as f64)
}
pub fn build_admin_proxy_nodes_list_payload_response(
items: Vec<serde_json::Value>,
total: usize,

View File

@@ -15,4 +15,5 @@ serde.workspace = true
serde_json.workspace = true
sha1 = "0.10"
sha2.workspace = true
url.workspace = true
uuid.workspace = true

View File

@@ -38,6 +38,161 @@ pub use crate::contracts::{
OPENAI_VIDEO_CREATE_SYNC_PLAN_KIND, OPENAI_VIDEO_DELETE_SYNC_PLAN_KIND,
OPENAI_VIDEO_REMIX_SYNC_PLAN_KIND,
};
pub use crate::formats::claude::messages::stream::{ClaudeClientEmitter, ClaudeProviderState};
pub use crate::formats::gemini::generate_content::stream::{
GeminiClientEmitter, GeminiProviderState,
};
pub use crate::formats::openai::chat::stream::{
OpenAIChatClientEmitter, OpenAIChatProviderState, OpenAIResponsesClientEmitter,
OpenAIResponsesProviderState,
};
pub use crate::formats::openai::image::stream::{
maybe_build_openai_image_sync_finalize_product, OpenAiImageStreamState,
OpenAiImageSyncFinalizeProduct,
};
pub use crate::formats::openai::shared::{
copy_request_number_field, copy_request_number_field_as,
map_openai_reasoning_effort_to_claude_output, map_openai_reasoning_effort_to_gemini_budget,
parse_openai_stop_sequences, resolve_openai_chat_max_tokens, value_as_u64,
};
pub use crate::formats::shared::error_body::{
build_core_error_body_for_client_format, is_core_error_finalize_kind, LocalCoreSyncErrorKind,
};
pub use crate::formats::shared::image_bridge::{
build_gemini_image_request_body_from_openai_image_request,
build_gemini_image_response_from_openai_image_response,
build_gemini_image_response_from_openai_responses_image_response,
build_openai_image_provider_body_from_response_stream_sync_body,
build_openai_image_request_body_from_gemini_image_request,
build_openai_image_response_from_gemini_response,
build_openai_image_response_from_response_stream_sync_body, gemini_request_is_image_generation,
resolve_requested_gemini_image_model_for_request, GeminiImageRequestForOpenAi,
OpenAiImageRequestForGemini,
};
pub use crate::formats::shared::model_directives::{
apply_model_directive_mapping_patch, apply_model_directive_overrides_from_model,
apply_model_directive_overrides_from_request, claude_model_uses_adaptive_effort,
extract_gemini_model_from_path, gemini_model_uses_thinking_level, model_directive_base_model,
normalize_model_directive_model, parse_model_directive, ModelDirective, ModelOverride,
ReasoningEffort,
};
pub use crate::formats::shared::passthrough::{
resolve_stream_spec as resolve_local_same_format_stream_spec,
resolve_sync_spec as resolve_local_same_format_sync_spec, LocalSameFormatProviderFamily,
LocalSameFormatProviderSpec,
};
pub use crate::formats::shared::request::{
endpoint_config_forces_upstream_stream_policy, enforce_request_body_stream_field,
force_upstream_streaming_for_provider, parse_direct_request_body,
resolve_upstream_is_stream_from_endpoint_config,
};
pub use crate::formats::shared::request_matrix::{
build_standard_request_body_from_canonical,
build_standard_request_body_from_canonical_with_model_directives,
};
pub use crate::formats::shared::response::{
build_generated_tool_call_id, build_local_success_background_report,
build_local_success_conversion_background_report, canonicalize_tool_arguments,
prepare_local_success_response_parts, prepare_local_success_response_parts_owned,
LocalSyncReportParts,
};
pub use crate::formats::shared::routing::{
is_matching_stream_http_request, is_matching_stream_request,
request_path_implies_stream_request, resolve_execution_runtime_stream_plan_kind,
resolve_execution_runtime_sync_plan_kind, sanitize_request_path,
sanitize_request_path_and_query, sanitize_request_query_string,
supports_stream_execution_decision_kind, supports_sync_execution_decision_kind,
};
pub use crate::formats::shared::sse::{encode_done_sse, encode_json_sse, map_claude_stop_reason};
pub use crate::formats::shared::standard_matrix::normalize_standard_request_to_openai_chat_request;
pub use crate::formats::shared::stream_core::common::*;
pub use crate::formats::shared::stream_core::{
CanonicalStreamFrame, StreamingStandardFormatMatrix, StreamingStandardTerminalObserver,
};
pub use crate::formats::shared::sync_products::{
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,
convert_standard_chat_response, convert_standard_cli_response,
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,
StandardCrossFormatSyncProduct, StandardSyncFinalizeNormalizedProduct,
};
pub use crate::formats::shared::sync_to_stream::{
maybe_bridge_standard_sync_json_to_stream, SyncToStreamBridgeOutcome,
};
pub use crate::formats::shared::{
maybe_build_ai_surface_stream_rewriter, resolve_finalize_stream_rewrite_mode,
AiSurfaceFinalizeError, AiSurfaceStreamRewriter, FinalizeStreamRewriteMode,
};
pub use crate::formats::{
claude::messages::{
resolve_stream_spec as resolve_claude_stream_spec,
resolve_sync_spec as resolve_claude_sync_spec,
},
gemini::generate_content::{
resolve_stream_spec as resolve_gemini_stream_spec,
resolve_sync_spec as resolve_gemini_sync_spec,
},
openai::responses::{
codex::{
apply_codex_openai_responses_special_body_edits,
apply_codex_openai_responses_special_headers,
apply_openai_responses_compact_special_body_edits, CODEX_OPENAI_IMAGE_DEFAULT_MODEL,
CODEX_OPENAI_IMAGE_DEFAULT_OUTPUT_FORMAT, CODEX_OPENAI_IMAGE_DEFAULT_VARIATION_MODEL,
CODEX_OPENAI_IMAGE_DEFAULT_VARIATION_PROMPT, CODEX_OPENAI_IMAGE_INTERNAL_MODEL,
},
spec::{
resolve_stream_spec as resolve_openai_responses_stream_spec,
resolve_sync_spec as resolve_openai_responses_sync_spec, LocalOpenAiResponsesSpec,
},
},
shared::{
family::{LocalStandardSourceFamily, LocalStandardSourceMode, LocalStandardSpec},
standard_matrix::{
build_standard_request_body, build_standard_request_body_with_model_directives,
build_standard_request_body_with_model_directives_and_request_headers,
},
standard_normalize::{
build_cross_format_openai_chat_request_body,
build_cross_format_openai_chat_request_body_with_model_directives,
build_cross_format_openai_responses_request_body,
build_cross_format_openai_responses_request_body_with_model_directives,
build_local_openai_chat_request_body,
build_local_openai_chat_request_body_with_model_directives,
build_local_openai_responses_request_body,
build_local_openai_responses_request_body_with_model_directives,
},
},
};
pub use crate::formats::{
gemini::files::spec::{
resolve_stream_spec as resolve_gemini_files_stream_spec,
resolve_sync_spec as resolve_gemini_files_sync_spec, LocalGeminiFilesSpec,
},
openai::image::{
request::{
build_chatgpt_web_image_request_body, build_openai_image_provider_request_body,
default_model_for_openai_image_operation, is_openai_image_stream_request,
normalize_openai_image_request, openai_image_operation_from_path,
resolve_requested_openai_image_model_for_request, ChatGptWebImageRequestError,
NormalizedOpenAiImageRequest, OpenAiImageOperation, OpenAiImageResponseFormat,
},
spec::{
resolve_stream_spec as resolve_local_image_stream_spec,
resolve_sync_spec as resolve_local_image_sync_spec, LocalOpenAiImageSpec,
},
},
shared::video::{
resolve_sync_spec as resolve_local_video_sync_spec, LocalVideoCreateFamily,
LocalVideoCreateSpec,
},
};
pub use crate::provider_compat::kiro_stream::{
build_kiro_final_message_sse_events, build_kiro_initial_sse_events,
build_kiro_stream_error_sse_events, calculate_kiro_context_input_tokens,
@@ -59,138 +214,14 @@ pub use crate::provider_compat::surfaces::{
ProviderAdaptationSurface, ANTIGRAVITY_V1INTERNAL_ENVELOPE_NAME,
GEMINI_CLI_V1INTERNAL_ENVELOPE_NAME, KIRO_ENVELOPE_NAME,
};
pub use crate::request::common::{
force_upstream_streaming_for_provider, parse_direct_request_body,
};
pub use crate::request::matrix::{
build_standard_request_body_from_canonical,
build_standard_request_body_from_canonical_with_model_directives,
};
pub use crate::request::model_directives::{
apply_model_directive_mapping_patch, apply_model_directive_overrides_from_model,
apply_model_directive_overrides_from_request, claude_model_uses_adaptive_effort,
extract_gemini_model_from_path, gemini_model_uses_thinking_level, model_directive_base_model,
normalize_model_directive_model, parse_model_directive, ModelDirective, ModelOverride,
ReasoningEffort,
};
pub use crate::request::openai::{
copy_request_number_field, copy_request_number_field_as,
map_openai_reasoning_effort_to_claude_output, map_openai_reasoning_effort_to_gemini_budget,
parse_openai_stop_sequences, resolve_openai_chat_max_tokens, value_as_u64,
};
pub use crate::request::passthrough::provider::{
resolve_stream_spec as resolve_local_same_format_stream_spec,
resolve_sync_spec as resolve_local_same_format_sync_spec, LocalSameFormatProviderFamily,
LocalSameFormatProviderSpec,
};
pub use crate::request::route::{
is_matching_stream_http_request, is_matching_stream_request,
resolve_execution_runtime_stream_plan_kind, resolve_execution_runtime_sync_plan_kind,
supports_stream_execution_decision_kind, supports_sync_execution_decision_kind,
};
pub use crate::request::specialized::{
files::{
resolve_stream_spec as resolve_gemini_files_stream_spec,
resolve_sync_spec as resolve_gemini_files_sync_spec, LocalGeminiFilesSpec,
},
image::{
build_chatgpt_web_image_request_body, build_openai_image_provider_request_body,
default_model_for_openai_image_operation, is_openai_image_stream_request,
normalize_openai_image_request, openai_image_operation_from_path,
resolve_requested_openai_image_model_for_request,
resolve_stream_spec as resolve_local_image_stream_spec,
resolve_sync_spec as resolve_local_image_sync_spec, ChatGptWebImageRequestError,
LocalOpenAiImageSpec, NormalizedOpenAiImageRequest, OpenAiImageOperation,
OpenAiImageResponseFormat,
},
video::{
resolve_sync_spec as resolve_local_video_sync_spec, LocalVideoCreateFamily,
LocalVideoCreateSpec,
},
};
pub use crate::request::standard::{
apply_codex_openai_responses_special_body_edits, apply_codex_openai_responses_special_headers,
apply_openai_responses_compact_special_body_edits, build_cross_format_openai_chat_request_body,
build_cross_format_openai_chat_request_body_with_model_directives,
build_cross_format_openai_responses_request_body,
build_cross_format_openai_responses_request_body_with_model_directives,
build_local_openai_chat_request_body,
build_local_openai_chat_request_body_with_model_directives,
build_local_openai_responses_request_body,
build_local_openai_responses_request_body_with_model_directives, build_standard_request_body,
build_standard_request_body_with_model_directives,
build_standard_request_body_with_model_directives_and_request_headers,
claude::{
resolve_stream_spec as resolve_claude_stream_spec,
resolve_sync_spec as resolve_claude_sync_spec,
},
gemini::{
resolve_stream_spec as resolve_gemini_stream_spec,
resolve_sync_spec as resolve_gemini_sync_spec,
},
normalize_standard_request_to_openai_chat_request,
openai_responses::{
resolve_stream_spec as resolve_openai_responses_stream_spec,
resolve_sync_spec as resolve_openai_responses_sync_spec, LocalOpenAiResponsesSpec,
},
LocalStandardSourceFamily, LocalStandardSourceMode, LocalStandardSpec,
CODEX_OPENAI_IMAGE_DEFAULT_MODEL, CODEX_OPENAI_IMAGE_DEFAULT_OUTPUT_FORMAT,
CODEX_OPENAI_IMAGE_DEFAULT_VARIATION_MODEL, CODEX_OPENAI_IMAGE_DEFAULT_VARIATION_PROMPT,
CODEX_OPENAI_IMAGE_INTERNAL_MODEL,
};
pub use crate::response::common::{
build_generated_tool_call_id, build_local_success_background_report,
build_local_success_conversion_background_report, canonicalize_tool_arguments,
prepare_local_success_response_parts, prepare_local_success_response_parts_owned,
LocalSyncReportParts,
};
pub use crate::response::error_body::{
build_core_error_body_for_client_format, is_core_error_finalize_kind, LocalCoreSyncErrorKind,
};
pub use crate::response::openai_image_stream::{
maybe_build_openai_image_sync_finalize_product, OpenAiImageStreamState,
OpenAiImageSyncFinalizeProduct,
};
pub use crate::response::sse::{encode_done_sse, encode_json_sse, map_claude_stop_reason};
pub use crate::response::standard::claude::stream::{ClaudeClientEmitter, ClaudeProviderState};
pub use crate::response::standard::gemini::stream::{GeminiClientEmitter, GeminiProviderState};
pub use crate::response::standard::openai::stream::{
OpenAIChatClientEmitter, OpenAIChatProviderState, OpenAIResponsesClientEmitter,
OpenAIResponsesProviderState,
};
pub use crate::response::standard::stream_core::common::*;
pub use crate::response::standard::stream_core::{
CanonicalStreamFrame, StreamingStandardFormatMatrix, StreamingStandardTerminalObserver,
};
pub use crate::response::sync_products::{
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,
convert_standard_chat_response, convert_standard_cli_response,
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,
StandardCrossFormatSyncProduct, StandardSyncFinalizeNormalizedProduct,
};
pub use crate::response::sync_to_stream::{
maybe_bridge_standard_sync_json_to_stream, SyncToStreamBridgeOutcome,
};
pub use crate::response::{
maybe_build_ai_surface_stream_rewriter, resolve_finalize_stream_rewrite_mode,
AiSurfaceFinalizeError, AiSurfaceStreamRewriter, FinalizeStreamRewriteMode,
};
pub use aether_ai_formats::protocol::conversion::request::{
pub use aether_ai_formats::formats::conversion::request::{
convert_openai_chat_request_to_claude_request, convert_openai_chat_request_to_gemini_request,
convert_openai_chat_request_to_openai_responses_request, extract_openai_text_content,
normalize_claude_request_to_openai_chat_request,
normalize_gemini_request_to_openai_chat_request,
normalize_openai_responses_request_to_openai_chat_request, parse_openai_tool_result_content,
};
pub use aether_ai_formats::protocol::conversion::response::{
pub use aether_ai_formats::formats::conversion::response::{
build_openai_responses_response, build_openai_responses_response_with_content,
build_openai_responses_response_with_reasoning, convert_claude_chat_response_to_openai_chat,
convert_claude_response_to_openai_responses, convert_gemini_chat_response_to_openai_chat,

View File

@@ -1,5 +1,5 @@
use crate::contracts::{CLAUDE_CHAT_STREAM_PLAN_KIND, CLAUDE_CHAT_SYNC_PLAN_KIND};
use crate::request::standard::family::{
use crate::formats::shared::family::{
LocalStandardSourceFamily, LocalStandardSourceMode, LocalStandardSpec,
};

View File

@@ -1,5 +1,5 @@
use crate::contracts::{CLAUDE_CLI_STREAM_PLAN_KIND, CLAUDE_CLI_SYNC_PLAN_KIND};
use crate::request::standard::family::{
use crate::formats::shared::family::{
LocalStandardSourceFamily, LocalStandardSourceMode, LocalStandardSpec,
};

View File

@@ -0,0 +1,16 @@
pub mod chat_spec;
pub mod cli_spec;
pub mod request;
pub mod response;
pub mod spec;
pub mod stream;
use crate::formats::shared::family::LocalStandardSpec;
pub fn resolve_sync_spec(plan_kind: &str) -> Option<LocalStandardSpec> {
chat_spec::resolve_sync_spec(plan_kind).or_else(|| cli_spec::resolve_sync_spec(plan_kind))
}
pub fn resolve_stream_spec(plan_kind: &str) -> Option<LocalStandardSpec> {
chat_spec::resolve_stream_spec(plan_kind).or_else(|| cli_spec::resolve_stream_spec(plan_kind))
}

View File

@@ -1,6 +1,14 @@
use serde_json::{json, Map, Value};
use crate::{
formats::{
context::FormatContext,
openai::shared::{
map_openai_reasoning_effort_to_claude_output,
map_openai_reasoning_effort_to_thinking_budget,
},
shared::model_directives::claude_model_uses_adaptive_effort,
},
protocol::canonical::{
canonical_extension_object_mut, canonical_instructions_to_claude_system,
canonical_messages_to_claude, canonical_openai_reasoning_effort,
@@ -11,14 +19,6 @@ use crate::{
compact_canonical_claude_messages, insert_f64, namespace_extension_object,
CanonicalRequest,
},
protocol::context::FormatContext,
request::{
model_directives::claude_model_uses_adaptive_effort,
openai::{
map_openai_reasoning_effort_to_claude_output,
map_openai_reasoning_effort_to_thinking_budget,
},
},
};
pub fn from(body: &Value, _ctx: &FormatContext) -> Option<CanonicalRequest> {

View File

@@ -3,13 +3,13 @@ use std::collections::BTreeMap;
use serde_json::{json, Value};
use crate::{
formats::context::FormatContext,
protocol::canonical::{
canonical_blocks_to_claude, canonical_stop_reason_to_claude, canonical_usage_to_claude,
claude_content_to_canonical_blocks, claude_extensions, claude_stop_reason_to_canonical,
claude_usage_to_canonical, namespace_extension_object, CanonicalResponse,
CanonicalResponseOutput, CanonicalRole,
},
protocol::context::FormatContext,
};
pub fn from(body: &Value, _ctx: &FormatContext) -> Option<CanonicalResponse> {

View File

@@ -0,0 +1 @@
pub use super::{chat_spec, cli_spec, resolve_stream_spec, resolve_sync_spec};

View File

@@ -2,13 +2,13 @@ use std::collections::BTreeMap;
use serde_json::{json, Map, Value};
use crate::response::common::{
use crate::formats::shared::response::{
build_generated_tool_call_id, canonicalize_tool_arguments,
remove_empty_pages_from_tool_arguments,
};
use crate::response::sse::{encode_json_sse, map_claude_stop_reason};
use crate::response::standard::stream_core::common::*;
use crate::response::AiSurfaceFinalizeError;
use crate::formats::shared::sse::{encode_json_sse, map_claude_stop_reason};
use crate::formats::shared::stream_core::common::*;
use crate::formats::shared::AiSurfaceFinalizeError;
#[derive(Default)]
struct ClaudeProviderToolState {

View File

@@ -0,0 +1 @@
pub mod messages;

View File

@@ -6,7 +6,7 @@
use serde_json::Value;
use crate::protocol::{context::FormatContext, registry};
use crate::formats::{context::FormatContext, registry};
pub fn convert_openai_chat_request_to_claude_request(
body_json: &Value,

View File

@@ -6,7 +6,7 @@
use serde_json::{json, Value};
use crate::protocol::{context::FormatContext, registry};
use crate::formats::{context::FormatContext, registry};
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct OpenAiResponsesResponseUsage {

View File

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

View File

@@ -0,0 +1,40 @@
use serde_json::Value;
use serde_json::{json, Map};
use crate::formats::context::FormatContext;
use crate::formats::openai::embedding::request::mapped_embedding_model;
use crate::protocol::canonical::{namespace_extension_object, CanonicalRequest};
pub fn to(request: &CanonicalRequest, ctx: &FormatContext) -> Option<Value> {
let embedding = request.embedding.as_ref()?;
let items = embedding.input.as_string_items()?;
if items.is_empty() || items.iter().any(|value| value.trim().is_empty()) {
return None;
}
let mut output = Map::new();
output.insert(
"model".to_string(),
Value::String(mapped_embedding_model(
request,
ctx.mapped_model_or(request.model.as_str()),
)),
);
output.insert(
"input".to_string(),
Value::Array(
items
.into_iter()
.map(|text| json!({"type": "text", "text": text}))
.collect(),
),
);
if let Some(dimensions) = embedding.dimensions {
output.insert("dimensions".to_string(), Value::from(dimensions));
}
output.extend(namespace_extension_object(
&embedding.extensions,
"doubao",
&output,
));
Some(Value::Object(output))
}

View File

@@ -0,0 +1 @@
pub mod embedding;

View File

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

View File

@@ -0,0 +1,34 @@
use serde_json::json;
use serde_json::Value;
use crate::formats::context::FormatContext;
use crate::formats::openai::embedding::request::mapped_embedding_model;
use crate::protocol::canonical::CanonicalRequest;
pub fn to(request: &CanonicalRequest, ctx: &FormatContext) -> Option<Value> {
let embedding = request.embedding.as_ref()?;
let items = embedding.input.as_string_items()?;
if items.is_empty() || items.iter().any(|value| value.trim().is_empty()) {
return None;
}
let model = mapped_embedding_model(request, ctx.mapped_model_or(request.model.as_str()));
if items.len() == 1 {
return Some(json!({
"model": model,
"content": {
"parts": [{"text": items[0]}]
}
}));
}
Some(json!({
"model": model,
"requests": items.into_iter().map(|text| {
json!({
"model": model,
"content": {
"parts": [{"text": text}]
}
})
}).collect::<Vec<_>>()
}))
}

View File

@@ -0,0 +1 @@
pub mod spec;

View File

@@ -1,5 +1,5 @@
use crate::contracts::{GEMINI_CHAT_STREAM_PLAN_KIND, GEMINI_CHAT_SYNC_PLAN_KIND};
use crate::request::standard::family::{
use crate::formats::shared::family::{
LocalStandardSourceFamily, LocalStandardSourceMode, LocalStandardSpec,
};

View File

@@ -1,5 +1,5 @@
use crate::contracts::{GEMINI_CLI_STREAM_PLAN_KIND, GEMINI_CLI_SYNC_PLAN_KIND};
use crate::request::standard::family::{
use crate::formats::shared::family::{
LocalStandardSourceFamily, LocalStandardSourceMode, LocalStandardSpec,
};

View File

@@ -0,0 +1,16 @@
pub mod chat_spec;
pub mod cli_spec;
pub mod request;
pub mod response;
pub mod spec;
pub mod stream;
use crate::formats::shared::family::LocalStandardSpec;
pub fn resolve_sync_spec(plan_kind: &str) -> Option<LocalStandardSpec> {
chat_spec::resolve_sync_spec(plan_kind).or_else(|| cli_spec::resolve_sync_spec(plan_kind))
}
pub fn resolve_stream_spec(plan_kind: &str) -> Option<LocalStandardSpec> {
chat_spec::resolve_stream_spec(plan_kind).or_else(|| cli_spec::resolve_stream_spec(plan_kind))
}

View File

@@ -3,6 +3,14 @@ use std::collections::BTreeMap;
use serde_json::{json, Map, Value};
use crate::{
formats::{
context::FormatContext,
openai::shared::{
map_openai_reasoning_effort_to_gemini_budget,
map_thinking_budget_to_openai_reasoning_effort,
},
shared::model_directives::{gemini_model_uses_thinking_level, ReasoningEffort},
},
protocol::canonical::{
apply_gemini_request_extensions, canonical_extension_object_mut,
canonical_openai_reasoning_effort, extract_gemini_model_from_path,
@@ -14,14 +22,6 @@ use crate::{
CanonicalResponseFormat, CanonicalRole, CanonicalToolChoice, CanonicalToolDefinition,
OPENAI_RESPONSES_LEGACY_EXTENSION_NAMESPACE,
},
protocol::context::FormatContext,
request::{
model_directives::{gemini_model_uses_thinking_level, ReasoningEffort},
openai::{
map_openai_reasoning_effort_to_gemini_budget,
map_thinking_budget_to_openai_reasoning_effort,
},
},
};
pub fn from(body: &Value, ctx: &FormatContext) -> Option<CanonicalRequest> {

View File

@@ -1,13 +1,13 @@
use serde_json::{json, Map, Value};
use crate::{
formats::context::FormatContext,
protocol::canonical::{
canonical_extension_object_mut, gemini_extensions, gemini_part_to_canonical_block,
gemini_stop_reason_to_canonical, gemini_usage_to_canonical, CanonicalContentBlock,
CanonicalResponse, CanonicalResponseOutput, CanonicalRole, CanonicalStopReason,
CanonicalUsage,
},
protocol::context::FormatContext,
};
pub fn from(body: &Value, _ctx: &FormatContext) -> Option<CanonicalResponse> {

View File

@@ -0,0 +1 @@
pub use super::{chat_spec, cli_spec, resolve_stream_spec, resolve_sync_spec};

View File

@@ -2,10 +2,10 @@ use std::collections::BTreeMap;
use serde_json::{json, Map, Value};
use crate::response::common::{build_generated_tool_call_id, canonicalize_tool_arguments};
use crate::response::sse::encode_json_sse;
use crate::response::standard::stream_core::common::*;
use crate::response::AiSurfaceFinalizeError;
use crate::formats::shared::response::{build_generated_tool_call_id, canonicalize_tool_arguments};
use crate::formats::shared::sse::encode_json_sse;
use crate::formats::shared::stream_core::common::*;
use crate::formats::shared::AiSurfaceFinalizeError;
#[derive(Default)]
struct GeminiProviderToolState {

View File

@@ -0,0 +1,4 @@
pub mod embedding;
pub mod files;
pub mod generate_content;
pub mod video;

View File

@@ -0,0 +1 @@
pub mod spec;

View File

@@ -0,0 +1,27 @@
use crate::contracts::GEMINI_VIDEO_CREATE_SYNC_PLAN_KIND;
use crate::formats::shared::video::{LocalVideoCreateFamily, LocalVideoCreateSpec};
pub fn resolve_sync_spec(plan_kind: &str) -> Option<LocalVideoCreateSpec> {
match plan_kind {
GEMINI_VIDEO_CREATE_SYNC_PLAN_KIND => Some(LocalVideoCreateSpec {
api_format: "gemini:video",
decision_kind: GEMINI_VIDEO_CREATE_SYNC_PLAN_KIND,
report_kind: "gemini_video_create_sync_finalize",
family: LocalVideoCreateFamily::Gemini,
}),
_ => None,
}
}
#[cfg(test)]
mod tests {
use super::{resolve_sync_spec, LocalVideoCreateFamily};
#[test]
fn resolves_gemini_video_create_spec() {
let spec = resolve_sync_spec("gemini_video_create_sync").expect("spec");
assert_eq!(spec.api_format, "gemini:video");
assert_eq!(spec.family, LocalVideoCreateFamily::Gemini);
assert_eq!(spec.report_kind, "gemini_video_create_sync_finalize");
}
}

View File

@@ -1,16 +1,7 @@
//! Format identity and per-wire-format adapters.
//!
//! Each child module owns the boundary between one external wire shape and
//! the canonical IR. Registry conversion is intentionally constrained to:
//! source format -> canonical -> target format.
//! Format identity and aliases.
use std::{fmt, str::FromStr};
pub mod claude_messages;
pub mod gemini_generate_content;
pub mod openai_chat;
pub mod openai_responses;
#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)]
pub enum FormatFamily {
OpenAi,
@@ -144,10 +135,18 @@ pub fn is_openai_responses_family_format(value: &str) -> bool {
)
}
pub fn api_format_uses_body_stream_field(value: &str) -> bool {
matches!(
FormatId::parse(value).map(FormatId::canonical),
Some(FormatId::OpenAiChat | FormatId::OpenAiResponses | FormatId::ClaudeMessages)
)
}
#[cfg(test)]
mod tests {
use super::{
api_format_alias_matches, api_format_storage_aliases, normalize_api_format_alias, FormatId,
api_format_alias_matches, api_format_storage_aliases, api_format_uses_body_stream_field,
normalize_api_format_alias, FormatId,
};
#[test]
@@ -313,4 +312,22 @@ mod tests {
vec!["doubao:embedding".to_string()]
);
}
#[test]
fn body_stream_field_support_matches_provider_wire_formats() {
assert!(api_format_uses_body_stream_field("openai:chat"));
assert!(api_format_uses_body_stream_field("/v1/chat/completions"));
assert!(api_format_uses_body_stream_field("openai:responses"));
assert!(api_format_uses_body_stream_field("/v1/responses"));
assert!(api_format_uses_body_stream_field("claude:messages"));
assert!(api_format_uses_body_stream_field("/v1/messages"));
assert!(!api_format_uses_body_stream_field(
"openai:responses:compact"
));
assert!(!api_format_uses_body_stream_field("/v1/responses/compact"));
assert!(!api_format_uses_body_stream_field(
"gemini:generate_content"
));
assert!(!api_format_uses_body_stream_field("openai:embedding"));
}
}

View File

@@ -0,0 +1,17 @@
use serde_json::Value;
use crate::formats::context::FormatContext;
use crate::protocol::canonical::CanonicalRequest;
pub fn from(body: &Value, _ctx: &FormatContext) -> Option<CanonicalRequest> {
crate::formats::openai::embedding::request::from_namespace(body, "jina")
}
pub fn to(request: &CanonicalRequest, ctx: &FormatContext) -> Option<Value> {
crate::formats::openai::embedding::request::to_openai_like(
request,
ctx.mapped_model_or(request.model.as_str()),
"jina",
true,
)
}

View File

@@ -0,0 +1,13 @@
use serde_json::Value;
use crate::protocol::canonical::CanonicalEmbeddingResponse;
pub fn from(body: &Value) -> Option<CanonicalEmbeddingResponse> {
crate::formats::openai::embedding::response::from_namespace(body, "jina")
}
pub fn to(response: &CanonicalEmbeddingResponse) -> Option<Value> {
Some(crate::formats::openai::embedding::response::to_openai_like(
response, "jina",
))
}

View File

@@ -0,0 +1,2 @@
pub mod embedding;
pub mod rerank;

View File

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

View File

@@ -0,0 +1,16 @@
use serde_json::Value;
use crate::formats::context::FormatContext;
use crate::protocol::canonical::CanonicalRequest;
pub fn from(body: &Value, _ctx: &FormatContext) -> Option<CanonicalRequest> {
crate::formats::openai::rerank::request::from_namespace(body, "jina")
}
pub fn to(request: &CanonicalRequest, ctx: &FormatContext) -> Option<Value> {
crate::formats::openai::rerank::request::to_openai_like(
request,
ctx.mapped_model_or(request.model.as_str()),
"jina",
)
}

View File

@@ -1,6 +1,6 @@
use crate::{
api_format_alias_matches,
protocol::formats::{is_openai_responses_compact_format, normalize_api_format_alias},
formats::id::{is_openai_responses_compact_format, normalize_api_format_alias},
};
#[derive(Debug, Clone, Copy, PartialEq, Eq)]

View File

@@ -0,0 +1,18 @@
pub mod claude;
pub mod context;
pub mod conversion;
pub mod doubao;
pub mod gemini;
pub mod id;
pub mod jina;
pub mod matrix;
pub mod openai;
pub mod registry;
pub mod shared;
pub use context::{FormatContext, FormatError};
pub use id::{
api_format_alias_matches, api_format_storage_aliases, is_openai_responses_compact_format,
is_openai_responses_family_format, is_openai_responses_format, normalize_api_format_alias,
FormatFamily, FormatId, FormatProfile,
};

View File

@@ -1,6 +1,7 @@
use serde_json::{json, Value};
use crate::{
formats::context::FormatContext,
protocol::canonical::{
canonical_extension_object_mut, canonical_message_to_openai_chat,
canonical_response_format_to_openai, canonical_tool_choice_to_openai,
@@ -11,7 +12,6 @@ use crate::{
CanonicalInstruction, CanonicalRequest, CanonicalRole, CanonicalThinkingConfig,
OPENAI_RESPONSES_EXTENSION_NAMESPACE, OPENAI_RESPONSES_LEGACY_EXTENSION_NAMESPACE,
},
protocol::context::FormatContext,
};
pub fn from(body: &Value, _ctx: &FormatContext) -> Option<CanonicalRequest> {

View File

@@ -3,6 +3,7 @@ use std::collections::BTreeMap;
use serde_json::{json, Value};
use crate::{
formats::context::FormatContext,
protocol::canonical::{
canonical_blocks_to_openai_chat_message, canonical_stop_reason_to_openai,
canonical_usage_to_openai, openai_extensions, openai_finish_reason_to_canonical,
@@ -10,7 +11,6 @@ use crate::{
CanonicalResponse, CanonicalResponseOutput, CanonicalRole,
OPENAI_RESPONSES_EXTENSION_NAMESPACE, OPENAI_RESPONSES_LEGACY_EXTENSION_NAMESPACE,
},
protocol::context::FormatContext,
};
pub fn from(body: &Value, _ctx: &FormatContext) -> Option<CanonicalResponse> {

View File

@@ -2,10 +2,10 @@ use std::collections::BTreeMap;
use serde_json::{json, Map, Value};
use crate::response::common::build_generated_tool_call_id;
use crate::response::sse::{encode_done_sse, encode_json_sse};
use crate::response::standard::stream_core::common::*;
use crate::response::AiSurfaceFinalizeError;
use crate::formats::shared::response::build_generated_tool_call_id;
use crate::formats::shared::sse::{encode_done_sse, encode_json_sse};
use crate::formats::shared::stream_core::common::*;
use crate::formats::shared::AiSurfaceFinalizeError;
#[derive(Default)]
struct OpenAIChatProviderToolState {

View File

@@ -0,0 +1,150 @@
use serde_json::Map;
use serde_json::Value;
use std::collections::BTreeMap;
use crate::formats::context::FormatContext;
use crate::protocol::canonical::{
namespace_extension_object, CanonicalEmbeddingInput, CanonicalEmbeddingRequest,
CanonicalRequest,
};
pub fn from(body: &Value, _ctx: &FormatContext) -> Option<CanonicalRequest> {
from_namespace(body, "openai")
}
pub fn to(request: &CanonicalRequest, ctx: &FormatContext) -> Option<Value> {
to_openai_like(
request,
ctx.mapped_model_or(request.model.as_str()),
"openai",
false,
)
}
pub(crate) fn from_namespace(body_json: &Value, namespace: &str) -> Option<CanonicalRequest> {
let request = body_json.as_object()?;
let model = request
.get("model")
.and_then(Value::as_str)
.map(str::trim)
.filter(|value| !value.is_empty())?
.to_string();
let input =
serde_json::from_value::<CanonicalEmbeddingInput>(request.get("input")?.clone()).ok()?;
if input.is_empty() {
return None;
}
let embedding = CanonicalEmbeddingRequest {
input,
encoding_format: request
.get("encoding_format")
.and_then(Value::as_str)
.map(ToOwned::to_owned),
dimensions: request.get("dimensions").and_then(Value::as_u64),
task: request
.get("task")
.and_then(Value::as_str)
.map(ToOwned::to_owned),
user: request
.get("user")
.and_then(Value::as_str)
.map(ToOwned::to_owned),
extensions: namespace_extensions(
namespace,
request,
&[
"model",
"input",
"encoding_format",
"dimensions",
"task",
"user",
],
),
};
Some(CanonicalRequest {
model,
embedding: Some(embedding),
..CanonicalRequest::default()
})
}
pub(crate) fn to_openai_like(
canonical: &CanonicalRequest,
mapped_model: &str,
namespace: &str,
default_task: bool,
) -> Option<Value> {
let embedding = canonical.embedding.as_ref()?;
if embedding.input.is_empty() {
return None;
}
let mut output = Map::new();
output.insert(
"model".to_string(),
Value::String(mapped_embedding_model(canonical, mapped_model)),
);
output.insert(
"input".to_string(),
serde_json::to_value(&embedding.input).ok()?,
);
if let Some(value) = &embedding.encoding_format {
output.insert("encoding_format".to_string(), Value::String(value.clone()));
}
if let Some(value) = embedding.dimensions {
output.insert("dimensions".to_string(), Value::from(value));
}
if let Some(value) = &embedding.user {
output.insert("user".to_string(), Value::String(value.clone()));
}
if let Some(task) = embedding
.task
.as_ref()
.filter(|value| !value.trim().is_empty())
{
output.insert("task".to_string(), Value::String(task.clone()));
} else if default_task {
output.insert(
"task".to_string(),
Value::String("text-matching".to_string()),
);
}
output.extend(namespace_extension_object(
&embedding.extensions,
namespace,
&output,
));
Some(Value::Object(output))
}
pub(crate) fn mapped_embedding_model(canonical: &CanonicalRequest, mapped_model: &str) -> String {
let mapped_model = mapped_model.trim();
if mapped_model.is_empty() {
canonical.model.clone()
} else {
mapped_model.to_string()
}
}
pub(crate) fn namespace_extensions(
namespace: &str,
object: &Map<String, Value>,
handled_keys: &[&str],
) -> BTreeMap<String, Value> {
let handled = handled_keys
.iter()
.copied()
.collect::<std::collections::BTreeSet<_>>();
let raw = object
.iter()
.filter(|(key, _)| !handled.contains(key.as_str()))
.map(|(key, value)| (key.clone(), value.clone()))
.collect::<Map<String, Value>>();
if raw.is_empty() {
BTreeMap::new()
} else {
BTreeMap::from([(namespace.to_string(), Value::Object(raw))])
}
}

View File

@@ -0,0 +1,106 @@
use serde_json::Value;
use serde_json::{json, Map};
use crate::formats::openai::embedding::request::namespace_extensions;
use crate::protocol::canonical::{
canonical_usage_to_openai, namespace_extension_object, openai_usage_to_canonical,
CanonicalEmbedding, CanonicalEmbeddingResponse,
};
pub fn from(body: &Value) -> Option<CanonicalEmbeddingResponse> {
from_namespace(body, "openai")
}
pub fn to(response: &CanonicalEmbeddingResponse) -> Option<Value> {
Some(to_openai_like(response, "openai"))
}
pub(crate) fn from_namespace(
body_json: &Value,
namespace: &str,
) -> Option<CanonicalEmbeddingResponse> {
let body = body_json.as_object()?;
if body.contains_key("error") {
return None;
}
let data = body.get("data")?.as_array()?;
let mut embeddings = Vec::new();
for (fallback_index, item) in data.iter().enumerate() {
let item_object = item.as_object()?;
let values = item_object.get("embedding")?.as_array()?;
let embedding = values
.iter()
.map(Value::as_f64)
.collect::<Option<Vec<_>>>()?;
embeddings.push(CanonicalEmbedding {
index: item_object
.get("index")
.and_then(Value::as_u64)
.and_then(|value| usize::try_from(value).ok())
.unwrap_or(fallback_index),
embedding,
extensions: namespace_extensions(
namespace,
item_object,
&["object", "index", "embedding"],
),
});
}
Some(CanonicalEmbeddingResponse {
id: body
.get("id")
.and_then(Value::as_str)
.unwrap_or("embd-unknown")
.to_string(),
model: body
.get("model")
.and_then(Value::as_str)
.unwrap_or("unknown")
.to_string(),
embeddings,
usage: openai_usage_to_canonical(body.get("usage")),
extensions: namespace_extensions(
namespace,
body,
&["id", "object", "model", "data", "usage"],
),
})
}
pub(crate) fn to_openai_like(canonical: &CanonicalEmbeddingResponse, namespace: &str) -> Value {
let mut response = Map::new();
response.insert("object".to_string(), Value::String("list".to_string()));
if !canonical.model.trim().is_empty() && canonical.model != "unknown" {
response.insert("model".to_string(), Value::String(canonical.model.clone()));
}
response.insert(
"data".to_string(),
Value::Array(
canonical
.embeddings
.iter()
.map(|embedding| {
let mut item = Map::new();
item.insert("object".to_string(), Value::String("embedding".to_string()));
item.insert("index".to_string(), Value::from(embedding.index as u64));
item.insert("embedding".to_string(), json!(embedding.embedding));
item.extend(namespace_extension_object(
&embedding.extensions,
namespace,
&item,
));
Value::Object(item)
})
.collect(),
),
);
if let Some(usage) = &canonical.usage {
response.insert("usage".to_string(), canonical_usage_to_openai(usage));
}
response.extend(namespace_extension_object(
&canonical.extensions,
namespace,
&response,
));
Value::Object(response)
}

View File

@@ -0,0 +1,3 @@
pub mod request;
pub mod spec;
pub mod stream;

View File

@@ -3,43 +3,10 @@ use std::collections::BTreeMap;
use base64::Engine as _;
use serde_json::{json, Map, Number, Value};
use crate::contracts::{OPENAI_IMAGE_STREAM_PLAN_KIND, OPENAI_IMAGE_SYNC_PLAN_KIND};
use crate::request::standard::{
use crate::formats::openai::responses::codex::{
CODEX_OPENAI_IMAGE_DEFAULT_MODEL, CODEX_OPENAI_IMAGE_DEFAULT_VARIATION_MODEL,
};
#[derive(Debug, Clone, Copy)]
pub struct LocalOpenAiImageSpec {
pub api_format: &'static str,
pub decision_kind: &'static str,
pub report_kind: &'static str,
pub require_streaming: bool,
}
pub fn resolve_sync_spec(plan_kind: &str) -> Option<LocalOpenAiImageSpec> {
match plan_kind {
OPENAI_IMAGE_SYNC_PLAN_KIND => Some(LocalOpenAiImageSpec {
api_format: "openai:image",
decision_kind: OPENAI_IMAGE_SYNC_PLAN_KIND,
report_kind: "openai_image_sync_finalize",
require_streaming: false,
}),
_ => None,
}
}
pub fn resolve_stream_spec(plan_kind: &str) -> Option<LocalOpenAiImageSpec> {
match plan_kind {
OPENAI_IMAGE_STREAM_PLAN_KIND => Some(LocalOpenAiImageSpec {
api_format: "openai:image",
decision_kind: OPENAI_IMAGE_STREAM_PLAN_KIND,
report_kind: "openai_image_stream_success",
require_streaming: true,
}),
_ => None,
}
}
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub enum OpenAiImageOperation {
Generate,
@@ -1134,10 +1101,10 @@ mod tests {
use super::{
build_chatgpt_web_image_request_body, build_openai_image_provider_request_body,
is_openai_image_stream_request, normalize_openai_image_request, resolve_stream_spec,
resolve_sync_spec, OpenAiImageOperation,
is_openai_image_stream_request, normalize_openai_image_request, OpenAiImageOperation,
};
use crate::request::standard::{
use crate::formats::openai::image::spec::{resolve_stream_spec, resolve_sync_spec};
use crate::formats::openai::responses::codex::{
apply_codex_openai_responses_special_body_edits, CODEX_OPENAI_IMAGE_INTERNAL_MODEL,
};

View File

@@ -0,0 +1,54 @@
use crate::contracts::{OPENAI_IMAGE_STREAM_PLAN_KIND, OPENAI_IMAGE_SYNC_PLAN_KIND};
#[derive(Debug, Clone, Copy)]
pub struct LocalOpenAiImageSpec {
pub api_format: &'static str,
pub decision_kind: &'static str,
pub report_kind: &'static str,
pub require_streaming: bool,
}
pub fn resolve_sync_spec(plan_kind: &str) -> Option<LocalOpenAiImageSpec> {
match plan_kind {
OPENAI_IMAGE_SYNC_PLAN_KIND => Some(LocalOpenAiImageSpec {
api_format: "openai:image",
decision_kind: OPENAI_IMAGE_SYNC_PLAN_KIND,
report_kind: "openai_image_sync_finalize",
require_streaming: false,
}),
_ => None,
}
}
pub fn resolve_stream_spec(plan_kind: &str) -> Option<LocalOpenAiImageSpec> {
match plan_kind {
OPENAI_IMAGE_STREAM_PLAN_KIND => Some(LocalOpenAiImageSpec {
api_format: "openai:image",
decision_kind: OPENAI_IMAGE_STREAM_PLAN_KIND,
report_kind: "openai_image_stream_success",
require_streaming: true,
}),
_ => None,
}
}
#[cfg(test)]
mod tests {
use super::{resolve_stream_spec, resolve_sync_spec};
#[test]
fn resolves_openai_image_sync_spec() {
let spec = resolve_sync_spec("openai_image_sync").expect("spec");
assert_eq!(spec.api_format, "openai:image");
assert_eq!(spec.report_kind, "openai_image_sync_finalize");
assert!(!spec.require_streaming);
}
#[test]
fn resolves_openai_image_stream_spec() {
let spec = resolve_stream_spec("openai_image_stream").expect("spec");
assert_eq!(spec.api_format, "openai:image");
assert_eq!(spec.report_kind, "openai_image_stream_success");
assert!(spec.require_streaming);
}
}

View File

@@ -2,9 +2,9 @@ use base64::Engine as _;
use serde_json::Value;
use crate::contracts::OPENAI_IMAGE_SYNC_FINALIZE_REPORT_KIND;
use crate::request::standard::CODEX_OPENAI_IMAGE_DEFAULT_OUTPUT_FORMAT;
use crate::response::sse::encode_json_sse;
use crate::response::AiSurfaceFinalizeError;
use crate::formats::openai::responses::codex::CODEX_OPENAI_IMAGE_DEFAULT_OUTPUT_FORMAT;
use crate::formats::shared::sse::encode_json_sse;
use crate::formats::shared::AiSurfaceFinalizeError;
#[derive(Default)]
pub struct OpenAiImageStreamState {
@@ -368,6 +368,7 @@ pub fn maybe_build_openai_image_sync_finalize_product(
report_kind: &str,
status_code: u16,
report_context: Option<&Value>,
body_json: Option<&Value>,
body_base64: Option<&str>,
) -> Result<Option<OpenAiImageSyncFinalizeProduct>, AiSurfaceFinalizeError> {
if report_kind != OPENAI_IMAGE_SYNC_FINALIZE_REPORT_KIND || status_code >= 400 {
@@ -384,6 +385,45 @@ pub fn maybe_build_openai_image_sync_finalize_product(
{
return Ok(None);
}
let provider_api_format = report_context
.get("provider_api_format")
.and_then(Value::as_str)
.map(str::trim)
.unwrap_or_default();
if provider_api_format == "gemini:generate_content" {
let Some(provider_body_json) = body_json else {
return Ok(None);
};
let Some(client_body_json) =
crate::formats::shared::image_bridge::build_openai_image_response_from_gemini_response(
provider_body_json,
Some(report_context),
)
else {
return Ok(None);
};
return Ok(Some(OpenAiImageSyncFinalizeProduct {
client_body_json,
provider_body_json: provider_body_json.clone(),
}));
}
if provider_api_format != "openai:image" {
return Ok(None);
}
if let Some(provider_body_json) = body_json {
if provider_body_json.get("output").is_some() && provider_body_json.get("data").is_none() {
let Some(client_body_json) = crate::formats::shared::image_bridge::build_openai_image_response_from_response_stream_sync_body(
provider_body_json,
Some(report_context),
) else {
return Ok(None);
};
return Ok(Some(OpenAiImageSyncFinalizeProduct {
client_body_json,
provider_body_json: provider_body_json.clone(),
}));
}
}
let Some(body_base64) = body_base64 else {
return Ok(None);
};
@@ -689,6 +729,7 @@ mod tests {
"openai_image_sync_finalize",
200,
Some(&report_context),
None,
Some(&body_base64),
)
.expect("finalize should succeed")

View File

@@ -0,0 +1,7 @@
pub mod chat;
pub mod embedding;
pub mod image;
pub mod rerank;
pub mod responses;
pub mod shared;
pub mod video;

View File

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

View File

@@ -0,0 +1,113 @@
use serde_json::Map;
use serde_json::Value;
use crate::formats::context::FormatContext;
use crate::formats::openai::embedding::request::namespace_extensions;
use crate::protocol::canonical::{
namespace_extension_object, CanonicalRequest, CanonicalRerankRequest,
};
pub fn from(body: &Value, _ctx: &FormatContext) -> Option<CanonicalRequest> {
from_namespace(body, "openai")
}
pub fn to(request: &CanonicalRequest, ctx: &FormatContext) -> Option<Value> {
to_openai_like(
request,
ctx.mapped_model_or(request.model.as_str()),
"openai",
)
}
pub(crate) fn from_namespace(body_json: &Value, namespace: &str) -> Option<CanonicalRequest> {
let request = body_json.as_object()?;
let model = request
.get("model")
.and_then(Value::as_str)
.map(str::trim)
.filter(|value| !value.is_empty())?
.to_string();
let query = request
.get("query")
.and_then(Value::as_str)
.map(str::trim)
.filter(|value| !value.is_empty())?
.to_string();
let documents = request.get("documents").and_then(Value::as_array)?.to_vec();
let rerank = CanonicalRerankRequest {
query,
documents,
top_n: request
.get("top_n")
.or_else(|| request.get("topN"))
.and_then(Value::as_u64),
return_documents: request
.get("return_documents")
.or_else(|| request.get("returnDocuments"))
.and_then(Value::as_bool),
extensions: namespace_extensions(
namespace,
request,
&[
"model",
"query",
"documents",
"top_n",
"topN",
"return_documents",
"returnDocuments",
],
),
};
if rerank.is_empty() || rerank.top_n == Some(0) {
return None;
}
Some(CanonicalRequest {
model,
rerank: Some(rerank),
..CanonicalRequest::default()
})
}
pub(crate) fn to_openai_like(
canonical: &CanonicalRequest,
mapped_model: &str,
namespace: &str,
) -> Option<Value> {
let rerank = canonical.rerank.as_ref()?;
if rerank.is_empty() || rerank.top_n == Some(0) {
return None;
}
let mut output = Map::new();
output.insert(
"model".to_string(),
Value::String(mapped_rerank_model(canonical, mapped_model)),
);
output.insert("query".to_string(), Value::String(rerank.query.clone()));
output.insert(
"documents".to_string(),
Value::Array(rerank.documents.clone()),
);
if let Some(value) = rerank.top_n {
output.insert("top_n".to_string(), Value::from(value));
}
if let Some(value) = rerank.return_documents {
output.insert("return_documents".to_string(), Value::Bool(value));
}
output.extend(namespace_extension_object(
&rerank.extensions,
namespace,
&output,
));
Some(Value::Object(output))
}
fn mapped_rerank_model(canonical: &CanonicalRequest, mapped_model: &str) -> String {
let mapped_model = mapped_model.trim();
if mapped_model.is_empty() {
canonical.model.clone()
} else {
mapped_model.to_string()
}
}

View File

@@ -281,8 +281,9 @@ pub fn apply_openai_responses_compact_special_body_edits(
return;
};
// `/v1/responses/compact` does not accept `store`.
// `/v1/responses/compact` does not accept `store` or body-level `stream`.
body_object.remove("store");
body_object.remove("stream");
}
pub fn apply_codex_openai_responses_special_body_edits(

View File

@@ -0,0 +1,5 @@
pub mod codex;
pub mod request;
pub mod response;
pub mod spec;
pub mod stream;

View File

@@ -1,6 +1,8 @@
use serde_json::{json, Map, Value};
use crate::{
formats::context::FormatContext,
formats::openai::shared::map_thinking_budget_to_openai_reasoning_effort,
protocol::canonical::{
canonical_response_format_to_openai, canonicalize_tool_arguments, media_data_or_url,
namespace_extension_object, openai_content_text, openai_extensions,
@@ -11,8 +13,6 @@ use crate::{
CanonicalThinkingConfig, CanonicalToolChoice, CanonicalToolDefinition,
OPENAI_RESPONSES_EXTENSION_NAMESPACE, OPENAI_RESPONSES_LEGACY_EXTENSION_NAMESPACE,
},
protocol::context::FormatContext,
request::openai::map_thinking_budget_to_openai_reasoning_effort,
};
pub fn from(body: &Value, _ctx: &FormatContext) -> Option<CanonicalRequest> {
@@ -188,6 +188,9 @@ pub fn to_raw(
OPENAI_RESPONSES_LEGACY_EXTENSION_NAMESPACE,
&output,
));
if compact {
output.remove("stream");
}
output.remove("verbosity");
Some(Value::Object(output))
}

View File

@@ -3,6 +3,7 @@ use std::collections::BTreeMap;
use serde_json::{json, Map, Value};
use crate::{
formats::context::FormatContext,
protocol::canonical::{
canonical_content_block_to_openai_responses_part,
canonical_usage_to_openai_responses_usage, canonicalize_tool_arguments,
@@ -12,7 +13,6 @@ use crate::{
CanonicalResponseOutput, CanonicalRole, CanonicalStopReason,
OPENAI_RESPONSES_EXTENSION_NAMESPACE, OPENAI_RESPONSES_LEGACY_EXTENSION_NAMESPACE,
},
protocol::context::FormatContext,
};
pub fn from(body: &Value, _ctx: &FormatContext) -> Option<CanonicalResponse> {

View File

@@ -0,0 +1,3 @@
pub use crate::formats::openai::chat::stream::{
OpenAIResponsesClientEmitter, OpenAIResponsesProviderState,
};

View File

@@ -1,6 +1,6 @@
use serde_json::{Map, Value};
use super::model_directives::ReasoningEffort;
use crate::formats::shared::model_directives::ReasoningEffort;
pub fn parse_openai_stop_sequences(stop: Option<&Value>) -> Option<Vec<Value>> {
match stop {

View File

@@ -0,0 +1 @@
pub mod spec;

View File

@@ -0,0 +1,27 @@
use crate::contracts::OPENAI_VIDEO_CREATE_SYNC_PLAN_KIND;
use crate::formats::shared::video::{LocalVideoCreateFamily, LocalVideoCreateSpec};
pub fn resolve_sync_spec(plan_kind: &str) -> Option<LocalVideoCreateSpec> {
match plan_kind {
OPENAI_VIDEO_CREATE_SYNC_PLAN_KIND => Some(LocalVideoCreateSpec {
api_format: "openai:video",
decision_kind: OPENAI_VIDEO_CREATE_SYNC_PLAN_KIND,
report_kind: "openai_video_create_sync_finalize",
family: LocalVideoCreateFamily::OpenAi,
}),
_ => None,
}
}
#[cfg(test)]
mod tests {
use super::{resolve_sync_spec, LocalVideoCreateFamily};
#[test]
fn resolves_openai_video_create_spec() {
let spec = resolve_sync_spec("openai_video_create_sync").expect("spec");
assert_eq!(spec.api_format, "openai:video");
assert_eq!(spec.family, LocalVideoCreateFamily::OpenAi);
assert_eq!(spec.report_kind, "openai_video_create_sync_finalize");
}
}

View File

@@ -1,17 +1,16 @@
use serde_json::Value;
use crate::{
protocol::canonical::{
canonical_to_embedding_request, canonical_to_rerank_request,
from_embedding_to_canonical_request, from_rerank_to_canonical_request, CanonicalRequest,
CanonicalResponse,
},
protocol::formats::{
claude_messages, gemini_generate_content, openai_chat, openai_responses, FormatId,
},
use crate::formats::{
claude::messages as claude_messages,
doubao,
gemini::{self, generate_content as gemini_generate_content},
id::FormatId,
jina,
openai::{self, chat as openai_chat, responses as openai_responses},
};
use crate::protocol::canonical::{CanonicalRequest, CanonicalResponse};
pub use crate::protocol::context::{FormatContext, FormatError};
pub use crate::formats::context::{FormatContext, FormatError};
pub fn parse_request(
source_format: &str,
@@ -26,10 +25,10 @@ pub fn parse_request(
}
FormatId::ClaudeMessages => claude_messages::request::from(body, ctx),
FormatId::GeminiGenerateContent => gemini_generate_content::request::from(body, ctx),
FormatId::OpenAiEmbedding => from_embedding_to_canonical_request(body, "openai"),
FormatId::JinaEmbedding => from_embedding_to_canonical_request(body, "jina"),
FormatId::OpenAiRerank => from_rerank_to_canonical_request(body, "openai"),
FormatId::JinaRerank => from_rerank_to_canonical_request(body, "jina"),
FormatId::OpenAiEmbedding => openai::embedding::request::from(body, ctx),
FormatId::JinaEmbedding => jina::embedding::request::from(body, ctx),
FormatId::OpenAiRerank => openai::rerank::request::from(body, ctx),
FormatId::JinaRerank => jina::rerank::request::from(body, ctx),
FormatId::GeminiEmbedding | FormatId::DoubaoEmbedding => None,
}
.ok_or_else(|| FormatError::RequestParseFailed {
@@ -57,36 +56,12 @@ pub fn emit_request(
FormatId::OpenAiResponsesCompact => openai_responses::request::to_compact(&request, ctx),
FormatId::ClaudeMessages => claude_messages::request::to(&request, ctx),
FormatId::GeminiGenerateContent => gemini_generate_content::request::to(&request, ctx),
FormatId::OpenAiEmbedding => canonical_to_embedding_request(
&request,
ctx.mapped_model_or(request.model.as_str()),
"openai",
),
FormatId::JinaEmbedding => canonical_to_embedding_request(
&request,
ctx.mapped_model_or(request.model.as_str()),
"jina",
),
FormatId::OpenAiRerank => canonical_to_rerank_request(
&request,
ctx.mapped_model_or(request.model.as_str()),
"openai",
),
FormatId::JinaRerank => canonical_to_rerank_request(
&request,
ctx.mapped_model_or(request.model.as_str()),
"jina",
),
FormatId::GeminiEmbedding => canonical_to_embedding_request(
&request,
ctx.mapped_model_or(request.model.as_str()),
"gemini",
),
FormatId::DoubaoEmbedding => canonical_to_embedding_request(
&request,
ctx.mapped_model_or(request.model.as_str()),
"doubao",
),
FormatId::OpenAiEmbedding => openai::embedding::request::to(&request, ctx),
FormatId::JinaEmbedding => jina::embedding::request::to(&request, ctx),
FormatId::OpenAiRerank => openai::rerank::request::to(&request, ctx),
FormatId::JinaRerank => jina::rerank::request::to(&request, ctx),
FormatId::GeminiEmbedding => gemini::embedding::request::to(&request, ctx),
FormatId::DoubaoEmbedding => doubao::embedding::request::to(&request, ctx),
}
.ok_or_else(|| FormatError::RequestEmitFailed {
format: target.as_str().to_string(),
@@ -197,7 +172,7 @@ mod tests {
use serde_json::json;
use super::{convert_request, FormatContext};
use crate::protocol::formats::FormatId;
use crate::formats::id::FormatId;
#[test]
fn openai_cli_alias_is_not_a_primary_format() {
@@ -347,7 +322,7 @@ mod tests {
] {
assert!(
!implementation.contains(forbidden),
"registry should dispatch through protocol::formats::<format> adapters, found {forbidden}"
"registry should dispatch through formats::<provider>::<surface> adapters, found {forbidden}"
);
}
}

File diff suppressed because it is too large Load Diff

View File

@@ -1,22 +1,30 @@
use std::fmt;
pub mod error_body;
pub mod family;
pub mod image_bridge;
pub mod model_directives;
pub mod passthrough;
pub mod request;
pub mod request_matrix;
pub mod response;
pub mod routing;
pub mod sse;
pub mod standard_matrix;
pub mod standard_normalize;
pub mod stream_core;
pub mod stream_rewrite;
pub mod sync_products;
pub mod sync_to_stream;
pub mod video;
pub use self::sse::{encode_done_sse, encode_json_sse, map_claude_stop_reason};
pub use self::standard::stream_core::CanonicalStreamEvent;
pub use self::standard::stream_core::CanonicalStreamFrame;
pub use self::stream_core::{CanonicalStreamEvent, CanonicalStreamFrame};
pub use self::stream_rewrite::{
maybe_build_ai_surface_stream_rewriter, resolve_finalize_stream_rewrite_mode,
AiSurfaceStreamRewriter, FinalizeStreamRewriteMode,
};
pub mod common;
pub mod error_body;
pub mod openai_image_stream;
pub mod sse;
pub mod standard;
pub mod stream_rewrite;
pub mod sync_products;
pub mod sync_to_stream;
#[derive(Debug)]
pub struct AiSurfaceFinalizeError(pub String);

View File

@@ -0,0 +1,383 @@
use base64::Engine as _;
use crate::formats::id::api_format_uses_body_stream_field;
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub(crate) enum UpstreamStreamPolicy {
Auto,
ForceStream,
ForceNonStream,
}
pub fn parse_direct_request_body(
is_json_request: bool,
body_bytes: &[u8],
) -> Option<(serde_json::Value, Option<String>)> {
if is_json_request {
if body_bytes.is_empty() {
Some((serde_json::json!({}), None))
} else {
serde_json::from_slice::<serde_json::Value>(body_bytes)
.ok()
.map(|value| (value, None))
}
} else {
Some((
serde_json::json!({}),
(!body_bytes.is_empty())
.then(|| base64::engine::general_purpose::STANDARD.encode(body_bytes)),
))
}
}
pub fn force_upstream_streaming_for_provider(
provider_type: &str,
provider_api_format: &str,
) -> bool {
provider_type.trim().eq_ignore_ascii_case("codex")
&& aether_ai_formats::is_openai_responses_format(provider_api_format)
}
pub(crate) fn parse_upstream_stream_policy(
value: Option<&serde_json::Value>,
) -> UpstreamStreamPolicy {
let Some(value) = value else {
return UpstreamStreamPolicy::Auto;
};
if let Some(value) = value.as_bool() {
return if value {
UpstreamStreamPolicy::ForceStream
} else {
UpstreamStreamPolicy::ForceNonStream
};
}
let serde_json::Value::String(value) = value else {
return UpstreamStreamPolicy::Auto;
};
let raw = value.trim().to_ascii_lowercase();
match raw.as_str() {
"" | "auto" | "follow" | "client" | "default" => UpstreamStreamPolicy::Auto,
"force_stream" | "stream" | "sse" | "true" | "1" | "yes" => {
UpstreamStreamPolicy::ForceStream
}
"force_non_stream" | "force_sync" | "non_stream" | "sync" | "false" | "0" | "no" => {
UpstreamStreamPolicy::ForceNonStream
}
_ => UpstreamStreamPolicy::Auto,
}
}
pub(crate) fn upstream_stream_policy_from_endpoint_config(
endpoint_config: Option<&serde_json::Value>,
) -> UpstreamStreamPolicy {
let Some(config) = endpoint_config.and_then(serde_json::Value::as_object) else {
return UpstreamStreamPolicy::Auto;
};
for key in [
"upstream_stream_policy",
"upstreamStreamPolicy",
"upstream_stream",
] {
if let Some(value) = config.get(key) {
return parse_upstream_stream_policy(Some(value));
}
}
UpstreamStreamPolicy::Auto
}
pub fn endpoint_config_forces_upstream_stream_policy(
endpoint_config: Option<&serde_json::Value>,
) -> bool {
matches!(
upstream_stream_policy_from_endpoint_config(endpoint_config),
UpstreamStreamPolicy::ForceStream | UpstreamStreamPolicy::ForceNonStream
)
}
/// Resolves the upstream provider transport mode.
///
/// `client_is_stream` means the request landed on a streaming surface or should
/// be treated as streaming; the original JSON body may not have had
/// `"stream": true`.
pub(crate) fn resolve_upstream_is_stream(
client_is_stream: bool,
hard_requires_streaming: bool,
policy: UpstreamStreamPolicy,
) -> bool {
// ForceStream is unconditional, while ForceNonStream yields to hard
// stream-only constraints such as Kiro or Codex OpenAI Responses.
match policy {
UpstreamStreamPolicy::ForceStream => true,
UpstreamStreamPolicy::ForceNonStream => hard_requires_streaming,
UpstreamStreamPolicy::Auto => hard_requires_streaming || client_is_stream,
}
}
pub fn enforce_request_body_stream_field(
body: &mut serde_json::Value,
provider_api_format: &str,
upstream_is_stream: bool,
require_body_stream_field: bool,
) {
let Some(body_object) = body.as_object_mut() else {
return;
};
if !api_format_uses_body_stream_field(provider_api_format) {
body_object.remove("stream");
return;
}
// Final-body fallback catches body rules, directive patches, and other
// provider-body mutations that introduce `stream`.
if upstream_is_stream || require_body_stream_field || body_object.contains_key("stream") {
body_object.insert(
"stream".to_string(),
serde_json::Value::Bool(upstream_is_stream),
);
} else {
body_object.remove("stream");
}
}
pub fn resolve_upstream_is_stream_from_endpoint_config(
endpoint_config: Option<&serde_json::Value>,
client_is_stream: bool,
hard_requires_streaming: bool,
) -> bool {
resolve_upstream_is_stream(
client_is_stream,
hard_requires_streaming,
upstream_stream_policy_from_endpoint_config(endpoint_config),
)
}
#[cfg(test)]
mod tests {
use super::{
endpoint_config_forces_upstream_stream_policy, enforce_request_body_stream_field,
force_upstream_streaming_for_provider, parse_direct_request_body,
parse_upstream_stream_policy, resolve_upstream_is_stream,
resolve_upstream_is_stream_from_endpoint_config,
upstream_stream_policy_from_endpoint_config, UpstreamStreamPolicy,
};
use serde_json::json;
#[test]
fn parses_empty_json_body_as_empty_object() {
assert_eq!(
parse_direct_request_body(true, b""),
Some((serde_json::json!({}), None))
);
}
#[test]
fn rejects_invalid_json_body() {
assert_eq!(parse_direct_request_body(true, b"{invalid"), None);
}
#[test]
fn encodes_non_json_body_as_base64() {
assert_eq!(
parse_direct_request_body(false, b"hello"),
Some((serde_json::json!({}), Some("aGVsbG8=".to_string())))
);
}
#[test]
fn forces_streaming_for_codex_openai_responses() {
assert!(force_upstream_streaming_for_provider(
"codex",
"openai:responses"
));
assert!(!force_upstream_streaming_for_provider(
"codex",
"openai:responses:compact"
));
}
#[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(
"openai",
"openai:responses"
));
}
#[test]
fn parses_python_compatible_upstream_stream_policy_values() {
assert_eq!(
parse_upstream_stream_policy(None),
UpstreamStreamPolicy::Auto
);
for value in [
json!(""),
json!("auto"),
json!("follow"),
json!("client"),
json!("default"),
json!("unknown"),
] {
assert_eq!(
parse_upstream_stream_policy(Some(&value)),
UpstreamStreamPolicy::Auto
);
}
for value in [
json!(true),
json!("force_stream"),
json!("stream"),
json!("sse"),
json!("true"),
json!("1"),
json!("yes"),
] {
assert_eq!(
parse_upstream_stream_policy(Some(&value)),
UpstreamStreamPolicy::ForceStream
);
}
for value in [
json!(false),
json!("force_non_stream"),
json!("force_sync"),
json!("non_stream"),
json!("sync"),
json!("false"),
json!("0"),
json!("no"),
] {
assert_eq!(
parse_upstream_stream_policy(Some(&value)),
UpstreamStreamPolicy::ForceNonStream
);
}
}
#[test]
fn parses_non_string_non_bool_policy_values_as_auto() {
for value in [json!(1), json!(0), json!(null), json!({}), json!([])] {
assert_eq!(
parse_upstream_stream_policy(Some(&value)),
UpstreamStreamPolicy::Auto
);
}
}
#[test]
fn enforces_request_body_stream_field_for_stream_and_streamless_formats() {
let mut openai_chat = json!({"stream": true});
enforce_request_body_stream_field(&mut openai_chat, "openai:chat", false, false);
assert_eq!(openai_chat.get("stream"), Some(&json!(false)));
let mut ordinary_sync = json!({"messages": []});
enforce_request_body_stream_field(&mut ordinary_sync, "openai:chat", false, false);
assert!(ordinary_sync.get("stream").is_none());
let mut forced_sync = json!({"messages": []});
enforce_request_body_stream_field(&mut forced_sync, "openai:chat", false, true);
assert_eq!(forced_sync.get("stream"), Some(&json!(false)));
let mut compact = json!({"stream": true});
enforce_request_body_stream_field(&mut compact, "openai:responses:compact", true, true);
assert!(compact.get("stream").is_none());
}
#[test]
fn reads_endpoint_policy_keys_in_python_compatible_order() {
assert_eq!(
upstream_stream_policy_from_endpoint_config(Some(&json!({
"upstream_stream_policy": "force_non_stream",
"upstreamStreamPolicy": "force_stream",
"upstream_stream": "force_stream"
}))),
UpstreamStreamPolicy::ForceNonStream
);
assert_eq!(
upstream_stream_policy_from_endpoint_config(Some(&json!({
"upstreamStreamPolicy": "force_stream"
}))),
UpstreamStreamPolicy::ForceStream
);
assert_eq!(
upstream_stream_policy_from_endpoint_config(Some(&json!({
"upstream_stream": false
}))),
UpstreamStreamPolicy::ForceNonStream
);
}
#[test]
fn detects_forced_endpoint_policy_values() {
assert!(endpoint_config_forces_upstream_stream_policy(Some(
&json!({"upstream_stream_policy": "force_stream"})
)));
assert!(endpoint_config_forces_upstream_stream_policy(Some(
&json!({"upstream_stream_policy": "force_non_stream"})
)));
assert!(!endpoint_config_forces_upstream_stream_policy(Some(
&json!({"upstream_stream_policy": "auto"})
)));
assert!(!endpoint_config_forces_upstream_stream_policy(None));
}
#[test]
fn resolves_upstream_stream_policy_against_client_mode_and_hard_constraints() {
assert!(resolve_upstream_is_stream(
false,
false,
UpstreamStreamPolicy::ForceStream
));
assert!(!resolve_upstream_is_stream(
true,
false,
UpstreamStreamPolicy::ForceNonStream
));
assert!(resolve_upstream_is_stream(
true,
true,
UpstreamStreamPolicy::ForceNonStream
));
assert!(!resolve_upstream_is_stream(
false,
false,
UpstreamStreamPolicy::Auto
));
assert!(resolve_upstream_is_stream(
true,
false,
UpstreamStreamPolicy::Auto
));
assert!(resolve_upstream_is_stream(
false,
true,
UpstreamStreamPolicy::Auto
));
}
#[test]
fn resolves_endpoint_policy_config_to_upstream_mode() {
assert!(resolve_upstream_is_stream_from_endpoint_config(
Some(&json!({"upstream_stream_policy": "force_stream"})),
false,
false,
));
assert!(!resolve_upstream_is_stream_from_endpoint_config(
Some(&json!({"upstream_stream_policy": "force_non_stream"})),
true,
false,
));
assert!(resolve_upstream_is_stream_from_endpoint_config(
Some(&json!({"upstream_stream_policy": "auto"})),
true,
false,
));
assert!(!resolve_upstream_is_stream_from_endpoint_config(
None, false, false,
));
}
}

View File

@@ -1,4 +1,4 @@
pub use crate::request::standard::matrix::{
pub use crate::formats::shared::standard_matrix::{
build_standard_request_body_from_canonical,
build_standard_request_body_from_canonical_with_model_directives,
};

View File

@@ -1,4 +1,5 @@
use http::Method;
use url::form_urlencoded;
use crate::contracts::{
CLAUDE_CHAT_STREAM_PLAN_KIND, CLAUDE_CHAT_SYNC_PLAN_KIND, CLAUDE_CLI_STREAM_PLAN_KIND,
@@ -14,7 +15,7 @@ use crate::contracts::{
OPENAI_VIDEO_CONTENT_PLAN_KIND, OPENAI_VIDEO_CREATE_SYNC_PLAN_KIND,
OPENAI_VIDEO_DELETE_SYNC_PLAN_KIND, OPENAI_VIDEO_REMIX_SYNC_PLAN_KIND,
};
use crate::request::specialized::image::is_openai_image_stream_request;
use crate::formats::openai::image::request::is_openai_image_stream_request;
pub fn resolve_execution_runtime_stream_plan_kind(
route_class: Option<&str>,
@@ -304,6 +305,67 @@ fn resolve_gemini_generate_content_plan_kind(
}
}
pub fn request_path_implies_stream_request(path: &str) -> bool {
let trimmed = path.trim();
let path = trimmed
.split_once('?')
.map(|(path, _)| path)
.unwrap_or(trimmed);
path.ends_with(":streamGenerateContent")
}
pub fn sanitize_request_path(path: &str) -> Option<String> {
let path = path
.trim()
.split_once('?')
.map(|(path, _)| path)
.unwrap_or_else(|| path.trim())
.trim();
(!path.is_empty()).then(|| path.to_string())
}
pub fn sanitize_request_query_string(query: &str) -> Option<String> {
let query = query.trim().trim_start_matches('?').trim();
if query.is_empty() {
return None;
}
let mut serializer = form_urlencoded::Serializer::new(String::new());
for (key, value) in form_urlencoded::parse(query.as_bytes()) {
if request_query_key_is_safe_to_trace(key.as_ref()) {
serializer.append_pair(key.as_ref(), value.as_ref());
}
}
let sanitized = serializer.finish();
(!sanitized.is_empty()).then_some(sanitized)
}
pub fn sanitize_request_path_and_query(path: &str, query: Option<&str>) -> Option<String> {
let trimmed = path.trim();
let (path, embedded_query) = trimmed
.split_once('?')
.map(|(path, query)| (path.trim(), Some(query)))
.unwrap_or((trimmed, None));
if path.is_empty() {
return None;
}
let sanitized_query = query
.and_then(sanitize_request_query_string)
.or_else(|| embedded_query.and_then(sanitize_request_query_string));
Some(match sanitized_query {
Some(query) => format!("{path}?{query}"),
None => path.to_string(),
})
}
fn request_query_key_is_safe_to_trace(key: &str) -> bool {
matches!(
key.to_ascii_lowercase().as_str(),
"alt" | "view" | "pagesize" | "page_size" | "limit" | "offset"
)
}
pub fn is_matching_stream_request(
plan_kind: &str,
path: &str,
@@ -320,7 +382,7 @@ pub fn is_matching_stream_request(
.and_then(|value| value.as_bool())
.unwrap_or(false),
GEMINI_CHAT_STREAM_PLAN_KIND | GEMINI_CLI_STREAM_PLAN_KIND => {
path.ends_with(":streamGenerateContent")
request_path_implies_stream_request(path)
}
_ => true,
}
@@ -388,7 +450,9 @@ mod tests {
use super::{
is_matching_stream_http_request, is_matching_stream_request,
resolve_execution_runtime_stream_plan_kind, resolve_execution_runtime_sync_plan_kind,
request_path_implies_stream_request, resolve_execution_runtime_stream_plan_kind,
resolve_execution_runtime_sync_plan_kind, sanitize_request_path,
sanitize_request_path_and_query, sanitize_request_query_string,
supports_stream_execution_decision_kind, supports_sync_execution_decision_kind,
};
use crate::contracts::{
@@ -609,6 +673,41 @@ mod tests {
);
}
#[test]
fn stream_path_detection_handles_gemini_method_paths_with_query() {
assert!(request_path_implies_stream_request(
"/v1beta/models/gemini-2.5-pro:streamGenerateContent?alt=sse"
));
assert!(request_path_implies_stream_request(
" /v1internal:streamGenerateContent?alt=sse "
));
assert!(!request_path_implies_stream_request(
"/v1beta/models/gemini-2.5-pro:generateContent?alt=sse"
));
}
#[test]
fn request_path_metadata_sanitizer_drops_sensitive_query_parameters() {
assert_eq!(
sanitize_request_path("/v1beta/models/gemini-2.5-pro:generateContent?key=secret")
.as_deref(),
Some("/v1beta/models/gemini-2.5-pro:generateContent")
);
assert_eq!(
sanitize_request_query_string("?key=secret&alt=sse&pageSize=10&token=hidden")
.as_deref(),
Some("alt=sse&pageSize=10")
);
assert_eq!(
sanitize_request_path_and_query(
"/v1beta/models/gemini-2.5-pro:streamGenerateContent?key=secret&alt=sse",
None
)
.as_deref(),
Some("/v1beta/models/gemini-2.5-pro:streamGenerateContent?alt=sse")
);
}
#[test]
fn stream_matching_requires_openai_stream_flag() {
assert!(!is_matching_stream_request(

View File

@@ -1,6 +1,6 @@
use serde_json::Value;
use crate::response::AiSurfaceFinalizeError;
use crate::formats::shared::AiSurfaceFinalizeError;
pub fn map_claude_stop_reason(
stop_reason: Option<&str>,

View File

@@ -1,23 +1,23 @@
use std::borrow::Cow;
use aether_ai_formats::protocol::conversion::request::{
use aether_ai_formats::formats::conversion::request::{
convert_openai_chat_request_to_claude_request, convert_openai_chat_request_to_gemini_request,
convert_openai_chat_request_to_openai_responses_request,
normalize_claude_request_to_openai_chat_request,
normalize_gemini_request_to_openai_chat_request,
normalize_openai_responses_request_to_openai_chat_request,
};
use aether_ai_formats::protocol::registry::{convert_request, FormatContext};
use aether_ai_formats::formats::registry::{convert_request, FormatContext};
use aether_ai_formats::provider_compat::proxy::rules::apply_local_body_rules_with_request_headers;
use serde_json::Value;
use crate::request::model_directives::apply_model_directive_overrides_from_request;
use crate::formats::shared::model_directives::apply_model_directive_overrides_from_request;
use super::{
use crate::formats::openai::responses::codex::{
apply_codex_openai_responses_special_body_edits,
apply_openai_responses_compact_special_body_edits,
codex::apply_codex_openai_responses_special_body_edits,
normalize::build_local_openai_chat_request_body_with_model_directives,
};
use crate::formats::shared::standard_normalize::build_local_openai_chat_request_body_with_model_directives;
#[allow(clippy::too_many_arguments)]
pub fn build_standard_request_body(
@@ -128,6 +128,18 @@ pub fn build_standard_request_body_with_model_directives_and_request_headers(
&mut provider_request_body,
provider_api_format,
);
let require_body_stream_field = body_json
.as_object()
.is_some_and(|object| object.contains_key("stream"))
|| provider_request_body
.as_object()
.is_some_and(|object| object.contains_key("stream"));
crate::formats::shared::request::enforce_request_body_stream_field(
&mut provider_request_body,
provider_api_format,
upstream_is_stream,
require_body_stream_field,
);
Some(provider_request_body)
}
@@ -302,13 +314,23 @@ mod tests {
fn assert_stream_flag(provider_api_format: &str, upstream_is_stream: bool, converted: &Value) {
match provider_api_format {
"openai:chat" | "openai:responses" | "claude:messages" => {
assert_eq!(
converted
.get("stream")
.and_then(Value::as_bool)
.unwrap_or(false),
upstream_is_stream,
"{provider_api_format} stream flag should follow upstream_is_stream"
if upstream_is_stream {
assert_eq!(
converted.get("stream").and_then(Value::as_bool),
Some(true),
"{provider_api_format} stream flag should be true for upstream streaming"
);
} else {
assert!(
converted.get("stream").is_none(),
"{provider_api_format} should not gain stream:false for ordinary sync requests"
);
}
}
"openai:responses:compact" => {
assert!(
converted.get("stream").is_none(),
"openai responses compact keeps stream out of the request body"
);
}
"gemini:generate_content" => {
@@ -321,6 +343,93 @@ mod tests {
}
}
fn assert_explicit_stream_flag(
provider_api_format: &str,
upstream_is_stream: bool,
converted: &Value,
) {
match provider_api_format {
"openai:chat" | "openai:responses" | "claude:messages" => {
assert_eq!(
converted.get("stream").and_then(Value::as_bool),
Some(upstream_is_stream),
"{provider_api_format} stream flag should follow upstream_is_stream"
);
}
"openai:responses:compact" | "gemini:generate_content" => {
assert!(converted.get("stream").is_none());
}
other => panic!("unexpected provider api format: {other}"),
}
}
#[test]
fn standard_request_body_overrides_client_stream_true_for_non_stream_upstream() {
let request = json!({
"model": "source-model",
"messages": [{"role": "user", "content": "hello"}],
"stream": true
});
for provider_api_format in [
"openai:chat",
"openai:responses",
"openai:responses:compact",
"claude:messages",
"gemini:generate_content",
] {
let converted = build_standard_request_body(
&request,
"openai:chat",
"mapped-model",
"custom",
provider_api_format,
"/v1/chat/completions",
false,
None,
None,
)
.unwrap_or_else(|| panic!("openai:chat -> {provider_api_format} should build"));
assert_explicit_stream_flag(provider_api_format, false, &converted);
}
}
#[test]
fn standard_request_body_stream_policy_wins_after_body_rules() {
let request = json!({
"model": "source-model",
"messages": [{"role": "user", "content": "hello"}],
"stream": true
});
let body_rules = json!([
{"action":"set","path":"stream","value":true}
]);
for provider_api_format in [
"openai:chat",
"openai:responses",
"openai:responses:compact",
"claude:messages",
"gemini:generate_content",
] {
let converted = build_standard_request_body(
&request,
"openai:chat",
"mapped-model",
"custom",
provider_api_format,
"/v1/chat/completions",
false,
Some(&body_rules),
None,
)
.unwrap_or_else(|| panic!("openai:chat -> {provider_api_format} should build"));
assert_explicit_stream_flag(provider_api_format, false, &converted);
}
}
fn codex_default_body_rules() -> Value {
json!([
{"action":"drop","path":"max_output_tokens"},

View File

@@ -1,4 +1,4 @@
use aether_ai_formats::protocol::conversion::request::{
use aether_ai_formats::formats::conversion::request::{
convert_openai_chat_request_to_claude_request, convert_openai_chat_request_to_gemini_request,
convert_openai_chat_request_to_openai_responses_request,
normalize_openai_responses_request_to_openai_chat_request,
@@ -6,7 +6,7 @@ use aether_ai_formats::protocol::conversion::request::{
use aether_ai_formats::{request_conversion_kind, RequestConversionKind};
use serde_json::{json, Value};
use crate::request::model_directives::apply_model_directive_overrides_from_request;
use crate::formats::shared::model_directives::apply_model_directive_overrides_from_request;
pub fn build_local_openai_chat_request_body(
body_json: &Value,
@@ -50,14 +50,24 @@ pub fn build_local_openai_chat_request_body_with_model_directives(
}
}
}
Some(with_model_directive_overrides(
let mut provider_request_body = with_model_directive_overrides(
Value::Object(provider_request_body),
"openai:chat",
mapped_model,
body_json,
None,
enable_model_directives,
))
);
let require_body_stream_field = body_json
.as_object()
.is_some_and(|object| object.contains_key("stream"));
crate::formats::shared::request::enforce_request_body_stream_field(
&mut provider_request_body,
"openai:chat",
upstream_is_stream,
require_body_stream_field,
);
Some(provider_request_body)
}
pub fn build_cross_format_openai_chat_request_body(
@@ -104,14 +114,24 @@ pub fn build_cross_format_openai_chat_request_body_with_model_directives(
}
_ => return None,
};
Some(with_model_directive_overrides(
let mut provider_request_body = with_model_directive_overrides(
provider_request_body,
provider_api_format,
mapped_model,
body_json,
None,
enable_model_directives,
))
);
let require_body_stream_field = body_json
.as_object()
.is_some_and(|object| object.contains_key("stream"));
crate::formats::shared::request::enforce_request_body_stream_field(
&mut provider_request_body,
provider_api_format,
upstream_is_stream,
require_body_stream_field,
);
Some(provider_request_body)
}
pub fn build_local_openai_responses_request_body(
@@ -143,14 +163,24 @@ pub fn build_local_openai_responses_request_body_with_model_directives(
if require_streaming {
provider_request_body.insert("stream".to_string(), Value::Bool(true));
}
Some(with_model_directive_overrides(
let mut provider_request_body = with_model_directive_overrides(
Value::Object(provider_request_body),
"openai:responses",
mapped_model,
body_json,
None,
enable_model_directives,
))
);
let require_body_stream_field = body_json
.as_object()
.is_some_and(|object| object.contains_key("stream"));
crate::formats::shared::request::enforce_request_body_stream_field(
&mut provider_request_body,
"openai:responses",
require_streaming,
require_body_stream_field,
);
Some(provider_request_body)
}
pub fn build_cross_format_openai_responses_request_body(
@@ -208,14 +238,24 @@ pub fn build_cross_format_openai_responses_request_body_with_model_directives(
upstream_is_stream,
)?,
};
Some(with_model_directive_overrides(
let mut provider_request_body = with_model_directive_overrides(
provider_request_body,
provider_api_format,
mapped_model,
body_json,
None,
enable_model_directives,
))
);
let require_body_stream_field = body_json
.as_object()
.is_some_and(|object| object.contains_key("stream"));
crate::formats::shared::request::enforce_request_body_stream_field(
&mut provider_request_body,
provider_api_format,
upstream_is_stream,
require_body_stream_field,
);
Some(provider_request_body)
}
fn with_model_directive_overrides(
@@ -324,6 +364,108 @@ mod tests {
);
}
#[test]
fn local_openai_chat_request_body_overrides_client_stream_for_non_stream_upstream() {
let body_json = json!({
"model": "gpt-5",
"messages": [{
"role": "user",
"content": "hello"
}],
"stream": true
});
let provider_request_body =
build_local_openai_chat_request_body(&body_json, "gpt-5-upstream", false)
.expect("openai chat body should build");
assert_eq!(provider_request_body["model"], "gpt-5-upstream");
assert_eq!(provider_request_body["stream"], false);
}
#[test]
fn local_openai_responses_request_body_overrides_client_stream_for_non_stream_upstream() {
let body_json = json!({
"model": "gpt-5",
"input": "hello",
"stream": true
});
let provider_request_body =
build_local_openai_responses_request_body(&body_json, "gpt-5-upstream", false)
.expect("openai responses body should build");
assert_eq!(provider_request_body["model"], "gpt-5-upstream");
assert_eq!(provider_request_body["stream"], false);
}
#[test]
fn cross_format_openai_chat_request_body_overrides_client_stream_for_non_stream_upstream() {
let body_json = json!({
"model": "gpt-5",
"messages": [{"role": "user", "content": "hello"}],
"stream": true
});
let claude = build_cross_format_openai_chat_request_body_with_model_directives(
&body_json,
"claude-sonnet-4-5",
"claude:messages",
false,
false,
)
.expect("claude body should build");
assert_eq!(claude["stream"], false);
let responses = build_cross_format_openai_chat_request_body_with_model_directives(
&body_json,
"gpt-5-upstream",
"openai:responses",
false,
false,
)
.expect("responses body should build");
assert_eq!(responses["stream"], false);
}
#[test]
fn cross_format_openai_chat_request_body_does_not_add_stream_false_for_plain_sync_body() {
let body_json = json!({
"model": "gpt-5",
"messages": [{"role": "user", "content": "hello"}]
});
let claude = build_cross_format_openai_chat_request_body_with_model_directives(
&body_json,
"claude-sonnet-4-5",
"claude:messages",
false,
false,
)
.expect("claude body should build");
assert!(claude.get("stream").is_none());
}
#[test]
fn cross_format_openai_responses_body_overrides_client_stream_for_non_stream_upstream() {
let body_json = json!({
"model": "gpt-5",
"input": "hello",
"stream": true
});
let provider_request_body = build_cross_format_openai_responses_request_body(
&body_json,
"claude-sonnet-4-5",
"openai:responses",
"claude:messages",
false,
)
.expect("claude body should build");
assert_eq!(provider_request_body["stream"], false);
}
#[test]
fn local_openai_chat_request_body_applies_reasoning_effort_suffix() {
let body_json = json!({

View File

@@ -2,20 +2,20 @@ use aether_ai_formats::FormatId;
use aether_contracts::{ExecutionStreamTerminalSummary, StandardizedUsage};
use serde_json::Value;
use crate::response::error_body::{
build_core_error_body_for_client_format, LocalCoreSyncErrorKind,
};
use crate::response::sse::encode_json_sse;
use crate::response::standard::claude::stream::{ClaudeClientEmitter, ClaudeProviderState};
use crate::response::standard::gemini::stream::{GeminiClientEmitter, GeminiProviderState};
use crate::response::standard::openai::stream::{
use crate::formats::claude::messages::stream::{ClaudeClientEmitter, ClaudeProviderState};
use crate::formats::gemini::generate_content::stream::{GeminiClientEmitter, GeminiProviderState};
use crate::formats::openai::chat::stream::{
OpenAIChatClientEmitter, OpenAIChatProviderState, OpenAIResponsesClientEmitter,
OpenAIResponsesProviderState,
};
use crate::response::standard::stream_core::common::{
use crate::formats::shared::error_body::{
build_core_error_body_for_client_format, LocalCoreSyncErrorKind,
};
use crate::formats::shared::sse::encode_json_sse;
use crate::formats::shared::stream_core::common::{
decode_json_data_line, CanonicalStreamEvent, CanonicalStreamFrame, CanonicalUsage,
};
use crate::response::AiSurfaceFinalizeError;
use crate::formats::shared::AiSurfaceFinalizeError;
#[derive(Default)]
pub struct StreamingStandardFormatMatrix {

View File

@@ -1,13 +1,13 @@
use serde_json::Value;
use crate::formats::openai::image::stream::OpenAiImageStreamState;
use crate::formats::shared::stream_core::StreamingStandardFormatMatrix;
use crate::formats::shared::AiSurfaceFinalizeError;
use crate::provider_compat::kiro_stream::KiroToClaudeCliStreamState;
use crate::provider_compat::private_envelope::transform_provider_private_stream_line;
use crate::provider_compat::surfaces::{
provider_adaptation_should_unwrap_stream_envelope, KIRO_ENVELOPE_NAME,
};
use crate::response::openai_image_stream::OpenAiImageStreamState;
use crate::response::standard::stream_core::StreamingStandardFormatMatrix;
use crate::response::AiSurfaceFinalizeError;
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum FinalizeStreamRewriteMode {

View File

@@ -1,13 +1,13 @@
use base64::Engine as _;
use std::collections::BTreeMap;
use aether_ai_formats::protocol::conversion::response::{
use aether_ai_formats::formats::conversion::response::{
convert_claude_response_to_openai_responses, convert_gemini_chat_response_to_openai_chat,
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,
};
use aether_ai_formats::protocol::registry::{convert_response, FormatContext};
use aether_ai_formats::formats::registry::{convert_response, FormatContext};
use aether_ai_formats::{
canonical_to_claude_response, canonical_to_gemini_response, canonical_to_openai_chat_response,
canonical_to_openai_responses_compact_response, canonical_to_openai_responses_response,
@@ -18,9 +18,9 @@ use aether_ai_formats::{
use serde_json::{json, Map, Value};
use super::AiSurfaceFinalizeError;
use crate::response::common::remove_empty_pages_from_tool_arguments;
use crate::response::standard::gemini::stream::GeminiProviderState;
use crate::response::standard::stream_core::common::{
use crate::formats::gemini::generate_content::stream::GeminiProviderState;
use crate::formats::shared::response::remove_empty_pages_from_tool_arguments;
use crate::formats::shared::stream_core::common::{
map_openai_finish_reason_to_gemini, parse_json_arguments_value, CanonicalContentPart,
CanonicalStreamEvent, CanonicalUsage,
};
@@ -595,6 +595,17 @@ pub fn maybe_build_standard_cross_format_sync_product(
let provider_api_format = provider_api_format.trim().to_ascii_lowercase();
let client_api_format = client_api_format.trim().to_ascii_lowercase();
if provider_api_format == "openai:image" && client_api_format == "gemini:generate_content" {
let client_body_json = crate::formats::shared::image_bridge::build_gemini_image_response_from_openai_responses_image_response(
&provider_body_json,
Some(report_context),
)?;
return Some(StandardCrossFormatSyncProduct {
client_body_json,
provider_body_json,
});
}
let client_body_json = if is_standard_chat_finalize_kind(report_kind) {
sync_chat_response_conversion_kind(&provider_api_format, &client_api_format)?;
convert_standard_chat_response(
@@ -2875,7 +2886,7 @@ mod tests {
maybe_build_standard_sync_finalize_product_from_normalized_payload,
StandardSyncFinalizeNormalizedProduct,
};
use aether_ai_formats::protocol::conversion::response::{
use aether_ai_formats::formats::conversion::response::{
convert_claude_chat_response_to_openai_chat, convert_gemini_chat_response_to_openai_chat,
convert_openai_chat_response_to_claude_chat, convert_openai_chat_response_to_gemini_chat,
convert_openai_chat_response_to_openai_responses,

View File

@@ -1,18 +1,18 @@
use aether_ai_formats::protocol::conversion::response::{
use aether_ai_formats::formats::conversion::response::{
convert_claude_response_to_openai_responses, convert_gemini_response_to_openai_responses,
convert_openai_chat_response_to_openai_responses,
};
use aether_contracts::{ExecutionStreamTerminalSummary, StandardizedUsage};
use serde_json::{json, Value};
use crate::response::sse::encode_json_sse;
use crate::response::standard::claude::stream::ClaudeClientEmitter;
use crate::response::standard::gemini::stream::GeminiClientEmitter;
use crate::response::standard::openai::stream::{
use crate::formats::claude::messages::stream::ClaudeClientEmitter;
use crate::formats::gemini::generate_content::stream::GeminiClientEmitter;
use crate::formats::openai::chat::stream::{
OpenAIChatClientEmitter, OpenAIResponsesClientEmitter, OpenAIResponsesProviderState,
};
use crate::response::standard::stream_core::CanonicalStreamFrame;
use crate::response::AiSurfaceFinalizeError;
use crate::formats::shared::sse::encode_json_sse;
use crate::formats::shared::stream_core::CanonicalStreamFrame;
use crate::formats::shared::AiSurfaceFinalizeError;
pub struct SyncToStreamBridgeOutcome {
pub sse_body: Vec<u8>,
@@ -27,7 +27,12 @@ pub fn maybe_bridge_standard_sync_json_to_stream(
) -> Result<Option<SyncToStreamBridgeOutcome>, AiSurfaceFinalizeError> {
let provider_api_format = normalize_api_format(provider_api_format);
let client_api_format = normalize_api_format(client_api_format);
if provider_api_format == "openai:image" && client_api_format == "openai:image" {
if client_api_format == "openai:image"
&& matches!(
provider_api_format.as_str(),
"openai:image" | "gemini:generate_content"
)
{
return maybe_bridge_openai_image_sync_json_to_stream(provider_body_json, report_context);
}
if !is_standard_api_format(provider_api_format.as_str())
@@ -67,6 +72,36 @@ fn maybe_bridge_openai_image_sync_json_to_stream(
provider_body_json: &Value,
report_context: Option<&Value>,
) -> Result<Option<SyncToStreamBridgeOutcome>, AiSurfaceFinalizeError> {
let provider_api_format = report_context
.and_then(|value| value.get("provider_api_format"))
.and_then(Value::as_str)
.map(str::trim)
.unwrap_or("openai:image");
let owned_response;
let provider_body_json = if provider_api_format == "gemini:generate_content" {
let Some(converted) =
crate::formats::shared::image_bridge::build_openai_image_response_from_gemini_response(
provider_body_json,
report_context,
)
else {
return Ok(None);
};
owned_response = converted;
&owned_response
} else if provider_body_json.get("output").is_some() && provider_body_json.get("data").is_none()
{
let Some(converted) = crate::formats::shared::image_bridge::build_openai_image_response_from_response_stream_sync_body(
provider_body_json,
report_context,
) else {
return Ok(None);
};
owned_response = converted;
&owned_response
} else {
provider_body_json
};
let Some(response) = provider_body_json.as_object() else {
return Ok(None);
};

View File

@@ -0,0 +1,34 @@
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum LocalVideoCreateFamily {
OpenAi,
Gemini,
}
#[derive(Debug, Clone, Copy)]
pub struct LocalVideoCreateSpec {
pub api_format: &'static str,
pub decision_kind: &'static str,
pub report_kind: &'static str,
pub family: LocalVideoCreateFamily,
}
pub fn resolve_sync_spec(plan_kind: &str) -> Option<LocalVideoCreateSpec> {
crate::formats::openai::video::spec::resolve_sync_spec(plan_kind)
.or_else(|| crate::formats::gemini::video::spec::resolve_sync_spec(plan_kind))
}
#[cfg(test)]
mod tests {
use super::{resolve_sync_spec, LocalVideoCreateFamily};
#[test]
fn resolves_openai_and_gemini_video_create_specs() {
let openai = resolve_sync_spec("openai_video_create_sync").expect("openai spec");
assert_eq!(openai.api_format, "openai:video");
assert_eq!(openai.family, LocalVideoCreateFamily::OpenAi);
let gemini = resolve_sync_spec("gemini_video_create_sync").expect("gemini spec");
assert_eq!(gemini.api_format, "gemini:video");
assert_eq!(gemini.family, LocalVideoCreateFamily::Gemini);
}
}

View File

@@ -2,11 +2,35 @@ extern crate self as aether_ai_formats;
pub mod api;
pub mod contracts;
pub mod formats;
pub mod protocol;
pub mod provider_compat;
pub mod request;
pub mod response;
pub use formats::context::{FormatContext, FormatError};
pub use formats::id::{
api_format_alias_matches, api_format_storage_aliases, api_format_uses_body_stream_field,
is_openai_responses_compact_format, is_openai_responses_family_format,
is_openai_responses_format, normalize_api_format_alias, FormatFamily, FormatId, FormatProfile,
};
pub use formats::matrix::{
is_embedding_api_format, is_rerank_api_format, request_candidate_api_format_preference,
request_candidate_api_formats, request_conversion_kind,
request_conversion_requires_enable_flag, sync_chat_response_conversion_kind,
sync_cli_response_conversion_kind, RequestConversionKind, SyncChatResponseConversionKind,
SyncCliResponseConversionKind,
};
pub use formats::registry::{build_stream_transcoder, convert_request, convert_response};
pub use formats::shared::model_directives::{
apply_model_directive_mapping_patch, apply_model_directive_overrides_from_model,
apply_model_directive_overrides_from_request, claude_model_uses_adaptive_effort,
extract_gemini_model_from_path, gemini_model_uses_thinking_level, model_directive_base_model,
normalize_model_directive_model, parse_model_directive, ModelDirective, ModelOverride,
ReasoningEffort,
};
pub use formats::shared::request::{
endpoint_config_forces_upstream_stream_policy, enforce_request_body_stream_field,
resolve_upstream_is_stream_from_endpoint_config,
};
pub use protocol::canonical::{
canonical_request_unknown_block_count, canonical_response_unknown_block_count,
canonical_to_claude_request, canonical_to_claude_response, canonical_to_embedding_response,
@@ -25,24 +49,3 @@ pub use protocol::canonical::{
CanonicalStopReason, CanonicalStreamEvent, CanonicalStreamFrame, CanonicalThinkingConfig,
CanonicalToolChoice, CanonicalToolDefinition, CanonicalUsage,
};
pub use protocol::context::{FormatContext, FormatError};
pub use protocol::formats::{
api_format_alias_matches, api_format_storage_aliases, is_openai_responses_compact_format,
is_openai_responses_family_format, is_openai_responses_format, normalize_api_format_alias,
FormatFamily, FormatId, FormatProfile,
};
pub use protocol::matrix::{
is_embedding_api_format, is_rerank_api_format, request_candidate_api_format_preference,
request_candidate_api_formats, request_conversion_kind,
request_conversion_requires_enable_flag, sync_chat_response_conversion_kind,
sync_cli_response_conversion_kind, RequestConversionKind, SyncChatResponseConversionKind,
SyncCliResponseConversionKind,
};
pub use protocol::registry::{build_stream_transcoder, convert_request, convert_response};
pub use request::model_directives::{
apply_model_directive_mapping_patch, apply_model_directive_overrides_from_model,
apply_model_directive_overrides_from_request, claude_model_uses_adaptive_effort,
extract_gemini_model_from_path, gemini_model_uses_thinking_level, model_directive_base_model,
normalize_model_directive_model, parse_model_directive, ModelDirective, ModelOverride,
ReasoningEffort,
};

View File

@@ -3,7 +3,7 @@ use std::collections::BTreeMap;
use serde::{Deserialize, Serialize};
use serde_json::{json, Map, Value};
use crate::request::openai::map_thinking_budget_to_openai_reasoning_effort;
use crate::formats::openai::shared::map_thinking_budget_to_openai_reasoning_effort;
pub use crate::protocol::stream::{CanonicalStreamEvent, CanonicalStreamFrame};
@@ -232,7 +232,7 @@ pub enum CanonicalEmbeddingInput {
}
impl CanonicalEmbeddingInput {
fn is_empty(&self) -> bool {
pub(crate) fn is_empty(&self) -> bool {
match self {
Self::String(value) => value.trim().is_empty(),
Self::StringArray(values) => {
@@ -243,7 +243,7 @@ impl CanonicalEmbeddingInput {
}
}
fn as_string_items(&self) -> Option<Vec<&str>> {
pub(crate) fn as_string_items(&self) -> Option<Vec<&str>> {
match self {
Self::String(value) => Some(vec![value.as_str()]),
Self::StringArray(values) => Some(values.iter().map(String::as_str).collect()),
@@ -281,7 +281,7 @@ pub struct CanonicalRerankRequest {
}
impl CanonicalRerankRequest {
fn is_empty(&self) -> bool {
pub(crate) fn is_empty(&self) -> bool {
self.query.trim().is_empty()
|| self.documents.is_empty()
|| self.documents.iter().any(rerank_document_is_empty)
@@ -385,15 +385,15 @@ pub struct CanonicalResponse {
}
pub fn from_openai_chat_to_canonical_request(body_json: &Value) -> Option<CanonicalRequest> {
crate::protocol::formats::openai_chat::request::from_raw(body_json)
crate::formats::openai::chat::request::from_raw(body_json)
}
pub fn canonical_to_openai_chat_request(canonical: &CanonicalRequest) -> Value {
crate::protocol::formats::openai_chat::request::to_raw(canonical)
crate::formats::openai::chat::request::to_raw(canonical)
}
pub fn from_openai_responses_to_canonical_request(body_json: &Value) -> Option<CanonicalRequest> {
crate::protocol::formats::openai_responses::request::from_raw(body_json)
crate::formats::openai::responses::request::from_raw(body_json)
}
pub(crate) fn canonical_to_openai_responses_request_with_profile(
@@ -402,7 +402,7 @@ pub(crate) fn canonical_to_openai_responses_request_with_profile(
upstream_is_stream: bool,
compact: bool,
) -> Option<Value> {
crate::protocol::formats::openai_responses::request::to_raw(
crate::formats::openai::responses::request::to_raw(
canonical,
mapped_model,
upstream_is_stream,
@@ -431,7 +431,7 @@ pub fn canonical_to_openai_responses_compact_request(
}
pub fn from_claude_to_canonical_request(body_json: &Value) -> Option<CanonicalRequest> {
crate::protocol::formats::claude_messages::request::from_raw(body_json)
crate::formats::claude::messages::request::from_raw(body_json)
}
pub fn canonical_to_claude_request(
@@ -439,18 +439,14 @@ pub fn canonical_to_claude_request(
mapped_model: &str,
upstream_is_stream: bool,
) -> Option<Value> {
crate::protocol::formats::claude_messages::request::to_raw(
canonical,
mapped_model,
upstream_is_stream,
)
crate::formats::claude::messages::request::to_raw(canonical, mapped_model, upstream_is_stream)
}
pub fn from_gemini_to_canonical_request(
body_json: &Value,
request_path: &str,
) -> Option<CanonicalRequest> {
crate::protocol::formats::gemini_generate_content::request::from_raw(body_json, request_path)
crate::formats::gemini::generate_content::request::from_raw(body_json, request_path)
}
pub fn canonical_to_gemini_request(
@@ -458,72 +454,59 @@ pub fn canonical_to_gemini_request(
mapped_model: &str,
upstream_is_stream: bool,
) -> Option<Value> {
crate::protocol::formats::gemini_generate_content::request::to_raw(
crate::formats::gemini::generate_content::request::to_raw(
canonical,
mapped_model,
upstream_is_stream,
)
}
#[cfg(test)]
pub(crate) fn from_embedding_to_canonical_request(
body_json: &Value,
namespace: &str,
) -> Option<CanonicalRequest> {
embedding_request_from_raw(body_json, namespace)
match namespace {
"openai" => crate::formats::openai::embedding::request::from_namespace(body_json, "openai"),
"jina" => crate::formats::openai::embedding::request::from_namespace(body_json, "jina"),
_ => None,
}
}
#[cfg(test)]
pub(crate) fn canonical_to_embedding_request(
canonical: &CanonicalRequest,
mapped_model: &str,
namespace: &str,
) -> Option<Value> {
let ctx = crate::formats::context::FormatContext::default().with_mapped_model(mapped_model);
match namespace {
"openai" => canonical_to_openai_embedding_request(canonical, mapped_model),
"jina" => canonical_to_jina_embedding_request(canonical, mapped_model),
"gemini" => canonical_to_gemini_embedding_request(canonical, mapped_model),
"doubao" => canonical_to_doubao_embedding_request(canonical, mapped_model),
_ => None,
}
}
pub(crate) fn from_rerank_to_canonical_request(
body_json: &Value,
namespace: &str,
) -> Option<CanonicalRequest> {
rerank_request_from_raw(body_json, namespace)
}
pub(crate) fn canonical_to_rerank_request(
canonical: &CanonicalRequest,
mapped_model: &str,
namespace: &str,
) -> Option<Value> {
match namespace {
"openai" | "jina" => {
canonical_to_openai_like_rerank_request(canonical, mapped_model, namespace)
}
"openai" => crate::formats::openai::embedding::request::to(canonical, &ctx),
"jina" => crate::formats::jina::embedding::request::to(canonical, &ctx),
"gemini" => crate::formats::gemini::embedding::request::to(canonical, &ctx),
"doubao" => crate::formats::doubao::embedding::request::to(canonical, &ctx),
_ => None,
}
}
pub fn from_openai_chat_to_canonical_response(body_json: &Value) -> Option<CanonicalResponse> {
crate::protocol::formats::openai_chat::response::from_raw(body_json)
crate::formats::openai::chat::response::from_raw(body_json)
}
pub fn from_openai_responses_to_canonical_response(body_json: &Value) -> Option<CanonicalResponse> {
crate::protocol::formats::openai_responses::response::from_raw(body_json)
crate::formats::openai::responses::response::from_raw(body_json)
}
pub fn from_claude_to_canonical_response(body_json: &Value) -> Option<CanonicalResponse> {
crate::protocol::formats::claude_messages::response::from_raw(body_json)
crate::formats::claude::messages::response::from_raw(body_json)
}
pub fn from_gemini_to_canonical_response(body_json: &Value) -> Option<CanonicalResponse> {
crate::protocol::formats::gemini_generate_content::response::from_raw(body_json)
crate::formats::gemini::generate_content::response::from_raw(body_json)
}
pub fn canonical_to_openai_chat_response(canonical: &CanonicalResponse) -> Value {
crate::protocol::formats::openai_chat::response::to_raw(canonical)
crate::formats::openai::chat::response::to_raw(canonical)
}
pub(crate) fn canonical_blocks_to_openai_chat_message(content: &[CanonicalContentBlock]) -> Value {
@@ -661,7 +644,7 @@ pub(crate) fn canonical_to_openai_responses_response_with_profile(
report_context: &Value,
compact: bool,
) -> Value {
crate::protocol::formats::openai_responses::response::to_raw(canonical, report_context, compact)
crate::formats::openai::responses::response::to_raw(canonical, report_context, compact)
}
pub fn canonical_to_openai_responses_response(
@@ -679,21 +662,27 @@ pub fn canonical_to_openai_responses_compact_response(
}
pub fn canonical_to_claude_response(canonical: &CanonicalResponse) -> Value {
crate::protocol::formats::claude_messages::response::to_raw(canonical)
crate::formats::claude::messages::response::to_raw(canonical)
}
pub fn canonical_to_gemini_response(
canonical: &CanonicalResponse,
report_context: &Value,
) -> Option<Value> {
crate::protocol::formats::gemini_generate_content::response::to_raw(canonical, report_context)
crate::formats::gemini::generate_content::response::to_raw(canonical, report_context)
}
pub fn from_embedding_to_canonical_response(
body_json: &Value,
namespace: &str,
) -> Option<CanonicalEmbeddingResponse> {
embedding_response_from_raw(body_json, namespace)
match namespace {
"openai" => {
crate::formats::openai::embedding::response::from_namespace(body_json, "openai")
}
"jina" => crate::formats::openai::embedding::response::from_namespace(body_json, "jina"),
_ => None,
}
}
pub fn canonical_to_embedding_response(
@@ -701,7 +690,8 @@ pub fn canonical_to_embedding_response(
namespace: &str,
) -> Option<Value> {
match namespace {
"openai" | "jina" => Some(canonical_to_openai_embedding_response(canonical, namespace)),
"openai" => crate::formats::openai::embedding::response::to(canonical),
"jina" => crate::formats::jina::embedding::response::to(canonical),
_ => None,
}
}
@@ -4273,140 +4263,6 @@ pub(crate) fn strip_claude_billing_header(text: &str) -> String {
remainder.trim_start_matches('\n').trim().to_string()
}
fn embedding_request_from_raw(body_json: &Value, namespace: &str) -> Option<CanonicalRequest> {
let request = body_json.as_object()?;
let model = request
.get("model")
.and_then(Value::as_str)
.map(str::trim)
.filter(|value| !value.is_empty())?
.to_string();
let input =
serde_json::from_value::<CanonicalEmbeddingInput>(request.get("input")?.clone()).ok()?;
if input.is_empty() {
return None;
}
let embedding = CanonicalEmbeddingRequest {
input,
encoding_format: request
.get("encoding_format")
.and_then(Value::as_str)
.map(ToOwned::to_owned),
dimensions: request.get("dimensions").and_then(Value::as_u64),
task: request
.get("task")
.and_then(Value::as_str)
.map(ToOwned::to_owned),
user: request
.get("user")
.and_then(Value::as_str)
.map(ToOwned::to_owned),
extensions: namespace_extensions(
namespace,
request,
&[
"model",
"input",
"encoding_format",
"dimensions",
"task",
"user",
],
),
};
Some(CanonicalRequest {
model,
embedding: Some(embedding),
..CanonicalRequest::default()
})
}
fn rerank_request_from_raw(body_json: &Value, namespace: &str) -> Option<CanonicalRequest> {
let request = body_json.as_object()?;
let model = request
.get("model")
.and_then(Value::as_str)
.map(str::trim)
.filter(|value| !value.is_empty())?
.to_string();
let query = request
.get("query")
.and_then(Value::as_str)
.map(str::trim)
.filter(|value| !value.is_empty())?
.to_string();
let documents = request.get("documents").and_then(Value::as_array)?.to_vec();
let rerank = CanonicalRerankRequest {
query,
documents,
top_n: request
.get("top_n")
.or_else(|| request.get("topN"))
.and_then(Value::as_u64),
return_documents: request
.get("return_documents")
.or_else(|| request.get("returnDocuments"))
.and_then(Value::as_bool),
extensions: namespace_extensions(
namespace,
request,
&[
"model",
"query",
"documents",
"top_n",
"topN",
"return_documents",
"returnDocuments",
],
),
};
if rerank.is_empty() || rerank.top_n == Some(0) {
return None;
}
Some(CanonicalRequest {
model,
rerank: Some(rerank),
..CanonicalRequest::default()
})
}
fn canonical_to_openai_like_rerank_request(
canonical: &CanonicalRequest,
mapped_model: &str,
namespace: &str,
) -> Option<Value> {
let rerank = canonical.rerank.as_ref()?;
if rerank.is_empty() || rerank.top_n == Some(0) {
return None;
}
let mut output = Map::new();
output.insert(
"model".to_string(),
Value::String(mapped_rerank_model(canonical, mapped_model)),
);
output.insert("query".to_string(), Value::String(rerank.query.clone()));
output.insert(
"documents".to_string(),
Value::Array(rerank.documents.clone()),
);
if let Some(value) = rerank.top_n {
output.insert("top_n".to_string(), Value::from(value));
}
if let Some(value) = rerank.return_documents {
output.insert("return_documents".to_string(), Value::Bool(value));
}
output.extend(namespace_extension_object(
&rerank.extensions,
namespace,
&output,
));
Some(Value::Object(output))
}
fn rerank_document_is_empty(value: &Value) -> bool {
match value {
Value::String(text) => text.trim().is_empty(),
@@ -4419,265 +4275,6 @@ fn rerank_document_is_empty(value: &Value) -> bool {
}
}
fn mapped_rerank_model(canonical: &CanonicalRequest, mapped_model: &str) -> String {
mapped_model
.trim()
.chars()
.next()
.map(|_| mapped_model.trim().to_string())
.unwrap_or_else(|| canonical.model.clone())
}
fn canonical_to_openai_embedding_request(
canonical: &CanonicalRequest,
mapped_model: &str,
) -> Option<Value> {
canonical_to_openai_like_embedding_request(canonical, mapped_model, "openai", false)
}
fn canonical_to_jina_embedding_request(
canonical: &CanonicalRequest,
mapped_model: &str,
) -> Option<Value> {
canonical_to_openai_like_embedding_request(canonical, mapped_model, "jina", true)
}
fn canonical_to_openai_like_embedding_request(
canonical: &CanonicalRequest,
mapped_model: &str,
namespace: &str,
default_task: bool,
) -> Option<Value> {
let embedding = canonical.embedding.as_ref()?;
if embedding.input.is_empty() {
return None;
}
let mut output = Map::new();
output.insert(
"model".to_string(),
Value::String(mapped_embedding_model(canonical, mapped_model)),
);
output.insert(
"input".to_string(),
serde_json::to_value(&embedding.input).ok()?,
);
if let Some(value) = &embedding.encoding_format {
output.insert("encoding_format".to_string(), Value::String(value.clone()));
}
if let Some(value) = embedding.dimensions {
output.insert("dimensions".to_string(), Value::from(value));
}
if let Some(value) = &embedding.user {
output.insert("user".to_string(), Value::String(value.clone()));
}
if let Some(task) = embedding
.task
.as_ref()
.filter(|value| !value.trim().is_empty())
{
output.insert("task".to_string(), Value::String(task.clone()));
} else if default_task {
output.insert(
"task".to_string(),
Value::String("text-matching".to_string()),
);
}
output.extend(namespace_extension_object(
&embedding.extensions,
namespace,
&output,
));
Some(Value::Object(output))
}
fn canonical_to_gemini_embedding_request(
canonical: &CanonicalRequest,
mapped_model: &str,
) -> Option<Value> {
let embedding = canonical.embedding.as_ref()?;
let items = embedding.input.as_string_items()?;
if items.is_empty() || items.iter().any(|value| value.trim().is_empty()) {
return None;
}
let model = mapped_embedding_model(canonical, mapped_model);
if items.len() == 1 {
return Some(json!({
"model": model,
"content": {
"parts": [{"text": items[0]}]
}
}));
}
Some(json!({
"model": model,
"requests": items.into_iter().map(|text| {
json!({
"model": model,
"content": {
"parts": [{"text": text}]
}
})
}).collect::<Vec<_>>()
}))
}
fn canonical_to_doubao_embedding_request(
canonical: &CanonicalRequest,
mapped_model: &str,
) -> Option<Value> {
let embedding = canonical.embedding.as_ref()?;
let items = embedding.input.as_string_items()?;
if items.is_empty() || items.iter().any(|value| value.trim().is_empty()) {
return None;
}
let mut output = Map::new();
output.insert(
"model".to_string(),
Value::String(mapped_embedding_model(canonical, mapped_model)),
);
output.insert(
"input".to_string(),
Value::Array(
items
.into_iter()
.map(|text| json!({"type": "text", "text": text}))
.collect(),
),
);
if let Some(dimensions) = embedding.dimensions {
output.insert("dimensions".to_string(), Value::from(dimensions));
}
output.extend(namespace_extension_object(
&embedding.extensions,
"doubao",
&output,
));
Some(Value::Object(output))
}
fn embedding_response_from_raw(
body_json: &Value,
namespace: &str,
) -> Option<CanonicalEmbeddingResponse> {
let body = body_json.as_object()?;
if body.contains_key("error") {
return None;
}
let data = body.get("data")?.as_array()?;
let mut embeddings = Vec::new();
for (fallback_index, item) in data.iter().enumerate() {
let item_object = item.as_object()?;
let values = item_object.get("embedding")?.as_array()?;
let embedding = values
.iter()
.map(Value::as_f64)
.collect::<Option<Vec<_>>>()?;
embeddings.push(CanonicalEmbedding {
index: item_object
.get("index")
.and_then(Value::as_u64)
.and_then(|value| usize::try_from(value).ok())
.unwrap_or(fallback_index),
embedding,
extensions: namespace_extensions(
namespace,
item_object,
&["object", "index", "embedding"],
),
});
}
Some(CanonicalEmbeddingResponse {
id: body
.get("id")
.and_then(Value::as_str)
.unwrap_or("embd-unknown")
.to_string(),
model: body
.get("model")
.and_then(Value::as_str)
.unwrap_or("unknown")
.to_string(),
embeddings,
usage: openai_usage_to_canonical(body.get("usage")),
extensions: namespace_extensions(
namespace,
body,
&["id", "object", "model", "data", "usage"],
),
})
}
fn canonical_to_openai_embedding_response(
canonical: &CanonicalEmbeddingResponse,
namespace: &str,
) -> Value {
let mut response = Map::new();
response.insert("object".to_string(), Value::String("list".to_string()));
if !canonical.model.trim().is_empty() && canonical.model != "unknown" {
response.insert("model".to_string(), Value::String(canonical.model.clone()));
}
response.insert(
"data".to_string(),
Value::Array(
canonical
.embeddings
.iter()
.map(|embedding| {
let mut item = Map::new();
item.insert("object".to_string(), Value::String("embedding".to_string()));
item.insert("index".to_string(), Value::from(embedding.index as u64));
item.insert("embedding".to_string(), json!(embedding.embedding));
item.extend(namespace_extension_object(
&embedding.extensions,
namespace,
&item,
));
Value::Object(item)
})
.collect(),
),
);
if let Some(usage) = &canonical.usage {
response.insert("usage".to_string(), canonical_usage_to_openai(usage));
}
response.extend(namespace_extension_object(
&canonical.extensions,
namespace,
&response,
));
Value::Object(response)
}
fn mapped_embedding_model(canonical: &CanonicalRequest, mapped_model: &str) -> String {
let mapped_model = mapped_model.trim();
if mapped_model.is_empty() {
canonical.model.clone()
} else {
mapped_model.to_string()
}
}
fn namespace_extensions(
namespace: &str,
object: &Map<String, Value>,
handled_keys: &[&str],
) -> BTreeMap<String, Value> {
let handled = handled_keys
.iter()
.copied()
.collect::<std::collections::BTreeSet<_>>();
let raw = object
.iter()
.filter(|(key, _)| !handled.contains(key.as_str()))
.map(|(key, value)| (key.clone(), value.clone()))
.collect::<Map<String, Value>>();
if raw.is_empty() {
BTreeMap::new()
} else {
BTreeMap::from([(namespace.to_string(), Value::Object(raw))])
}
}
#[cfg(test)]
mod tests {
use super::{

View File

@@ -1,8 +0,0 @@
//! Pairwise format conversion entry points.
//!
//! The public helpers in this module route through the registry, so request and
//! response conversion still pass through the typed canonical IR before a target
//! wire format is emitted.
pub mod request;
pub mod response;

View File

@@ -1,7 +1,2 @@
pub mod canonical;
pub mod context;
pub mod conversion;
pub mod formats;
pub mod matrix;
pub mod registry;
pub mod stream;

View File

@@ -6,7 +6,7 @@ use crate::provider_compat::kiro_stream::{
find_kiro_real_thinking_start_tag, KIRO_MAX_THINKING_BUFFER,
};
use crate::response::AiSurfaceFinalizeError;
use crate::formats::shared::AiSurfaceFinalizeError;
use super::super::AwsEventFrame;
use super::super::KiroClaudeStreamState;

View File

@@ -3,7 +3,7 @@ use crate::provider_compat::kiro_stream::{
find_kiro_real_thinking_end_tag_at_buffer_end,
};
use crate::response::AiSurfaceFinalizeError;
use crate::formats::shared::AiSurfaceFinalizeError;
use super::super::KiroClaudeStreamState;

View File

@@ -1,10 +1,10 @@
use serde_json::Value;
use uuid::Uuid;
use crate::formats::shared::AiSurfaceFinalizeError;
use crate::provider_compat::kiro_stream::{
build_kiro_initial_sse_events, build_kiro_stream_error_sse_events, encode_kiro_sse_events,
};
use crate::response::AiSurfaceFinalizeError;
use super::super::{EventStreamDecoder, KiroClaudeStreamState, KiroToClaudeCliStreamState};

View File

@@ -2,8 +2,8 @@ use std::collections::BTreeMap;
use serde_json::Value;
use crate::formats::shared::AiSurfaceFinalizeError;
use crate::provider_compat::kiro_stream::KiroToClaudeCliStreamState;
use crate::response::AiSurfaceFinalizeError;
use super::surfaces::{
provider_adaptation_allows_sync_finalize_envelope, provider_adaptation_descriptor_for_envelope,

View File

@@ -1,80 +0,0 @@
use base64::Engine as _;
pub fn parse_direct_request_body(
is_json_request: bool,
body_bytes: &[u8],
) -> Option<(serde_json::Value, Option<String>)> {
if is_json_request {
if body_bytes.is_empty() {
Some((serde_json::json!({}), None))
} else {
serde_json::from_slice::<serde_json::Value>(body_bytes)
.ok()
.map(|value| (value, None))
}
} else {
Some((
serde_json::json!({}),
(!body_bytes.is_empty())
.then(|| base64::engine::general_purpose::STANDARD.encode(body_bytes)),
))
}
}
pub fn force_upstream_streaming_for_provider(
provider_type: &str,
provider_api_format: &str,
) -> bool {
provider_type.trim().eq_ignore_ascii_case("codex")
&& aether_ai_formats::is_openai_responses_format(provider_api_format)
}
#[cfg(test)]
mod tests {
use super::{force_upstream_streaming_for_provider, parse_direct_request_body};
#[test]
fn parses_empty_json_body_as_empty_object() {
assert_eq!(
parse_direct_request_body(true, b""),
Some((serde_json::json!({}), None))
);
}
#[test]
fn rejects_invalid_json_body() {
assert_eq!(parse_direct_request_body(true, b"{invalid"), None);
}
#[test]
fn encodes_non_json_body_as_base64() {
assert_eq!(
parse_direct_request_body(false, b"hello"),
Some((serde_json::json!({}), Some("aGVsbG8=".to_string())))
);
}
#[test]
fn forces_streaming_for_codex_openai_responses() {
assert!(force_upstream_streaming_for_provider(
"codex",
"openai:responses"
));
assert!(!force_upstream_streaming_for_provider(
"codex",
"openai:responses:compact"
));
}
#[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(
"openai",
"openai:responses"
));
}
}

View File

@@ -1,8 +0,0 @@
pub mod common;
pub mod matrix;
pub mod model_directives;
pub mod openai;
pub mod passthrough;
pub mod route;
pub mod specialized;
pub mod standard;

View File

@@ -1 +0,0 @@
pub mod provider;

View File

@@ -1,3 +0,0 @@
pub mod files;
pub mod image;
pub mod video;

View File

@@ -1,54 +0,0 @@
use crate::contracts::{GEMINI_VIDEO_CREATE_SYNC_PLAN_KIND, OPENAI_VIDEO_CREATE_SYNC_PLAN_KIND};
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum LocalVideoCreateFamily {
OpenAi,
Gemini,
}
#[derive(Debug, Clone, Copy)]
pub struct LocalVideoCreateSpec {
pub api_format: &'static str,
pub decision_kind: &'static str,
pub report_kind: &'static str,
pub family: LocalVideoCreateFamily,
}
pub fn resolve_sync_spec(plan_kind: &str) -> Option<LocalVideoCreateSpec> {
match plan_kind {
OPENAI_VIDEO_CREATE_SYNC_PLAN_KIND => Some(LocalVideoCreateSpec {
api_format: "openai:video",
decision_kind: OPENAI_VIDEO_CREATE_SYNC_PLAN_KIND,
report_kind: "openai_video_create_sync_finalize",
family: LocalVideoCreateFamily::OpenAi,
}),
GEMINI_VIDEO_CREATE_SYNC_PLAN_KIND => Some(LocalVideoCreateSpec {
api_format: "gemini:video",
decision_kind: GEMINI_VIDEO_CREATE_SYNC_PLAN_KIND,
report_kind: "gemini_video_create_sync_finalize",
family: LocalVideoCreateFamily::Gemini,
}),
_ => None,
}
}
#[cfg(test)]
mod tests {
use super::{resolve_sync_spec, LocalVideoCreateFamily};
#[test]
fn resolves_openai_video_create_spec() {
let spec = resolve_sync_spec("openai_video_create_sync").expect("spec");
assert_eq!(spec.api_format, "openai:video");
assert_eq!(spec.family, LocalVideoCreateFamily::OpenAi);
assert_eq!(spec.report_kind, "openai_video_create_sync_finalize");
}
#[test]
fn resolves_gemini_video_create_spec() {
let spec = resolve_sync_spec("gemini_video_create_sync").expect("spec");
assert_eq!(spec.api_format, "gemini:video");
assert_eq!(spec.family, LocalVideoCreateFamily::Gemini);
assert_eq!(spec.report_kind, "gemini_video_create_sync_finalize");
}
}

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