mirror of
https://github.com/fawney19/Aether.git
synced 2026-09-02 01:10:23 +08:00
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:
@@ -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)
|
||||
|
||||
@@ -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"));
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -15,4 +15,5 @@ serde.workspace = true
|
||||
serde_json.workspace = true
|
||||
sha1 = "0.10"
|
||||
sha2.workspace = true
|
||||
url.workspace = true
|
||||
uuid.workspace = true
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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,
|
||||
};
|
||||
|
||||
@@ -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,
|
||||
};
|
||||
|
||||
16
crates/aether-ai-formats/src/formats/claude/messages/mod.rs
Normal file
16
crates/aether-ai-formats/src/formats/claude/messages/mod.rs
Normal 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))
|
||||
}
|
||||
@@ -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> {
|
||||
@@ -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> {
|
||||
@@ -0,0 +1 @@
|
||||
pub use super::{chat_spec, cli_spec, resolve_stream_spec, resolve_sync_spec};
|
||||
@@ -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 {
|
||||
1
crates/aether-ai-formats/src/formats/claude/mod.rs
Normal file
1
crates/aether-ai-formats/src/formats/claude/mod.rs
Normal file
@@ -0,0 +1 @@
|
||||
pub mod messages;
|
||||
@@ -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,
|
||||
@@ -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 {
|
||||
@@ -0,0 +1 @@
|
||||
pub mod request;
|
||||
@@ -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))
|
||||
}
|
||||
1
crates/aether-ai-formats/src/formats/doubao/mod.rs
Normal file
1
crates/aether-ai-formats/src/formats/doubao/mod.rs
Normal file
@@ -0,0 +1 @@
|
||||
pub mod embedding;
|
||||
@@ -0,0 +1 @@
|
||||
pub mod request;
|
||||
@@ -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<_>>()
|
||||
}))
|
||||
}
|
||||
1
crates/aether-ai-formats/src/formats/gemini/files/mod.rs
Normal file
1
crates/aether-ai-formats/src/formats/gemini/files/mod.rs
Normal file
@@ -0,0 +1 @@
|
||||
pub mod spec;
|
||||
@@ -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,
|
||||
};
|
||||
|
||||
@@ -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,
|
||||
};
|
||||
|
||||
@@ -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))
|
||||
}
|
||||
@@ -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> {
|
||||
@@ -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> {
|
||||
@@ -0,0 +1 @@
|
||||
pub use super::{chat_spec, cli_spec, resolve_stream_spec, resolve_sync_spec};
|
||||
@@ -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 {
|
||||
4
crates/aether-ai-formats/src/formats/gemini/mod.rs
Normal file
4
crates/aether-ai-formats/src/formats/gemini/mod.rs
Normal file
@@ -0,0 +1,4 @@
|
||||
pub mod embedding;
|
||||
pub mod files;
|
||||
pub mod generate_content;
|
||||
pub mod video;
|
||||
1
crates/aether-ai-formats/src/formats/gemini/video/mod.rs
Normal file
1
crates/aether-ai-formats/src/formats/gemini/video/mod.rs
Normal file
@@ -0,0 +1 @@
|
||||
pub mod spec;
|
||||
27
crates/aether-ai-formats/src/formats/gemini/video/spec.rs
Normal file
27
crates/aether-ai-formats/src/formats/gemini/video/spec.rs
Normal 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");
|
||||
}
|
||||
}
|
||||
@@ -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"));
|
||||
}
|
||||
}
|
||||
@@ -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,
|
||||
)
|
||||
}
|
||||
@@ -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",
|
||||
))
|
||||
}
|
||||
2
crates/aether-ai-formats/src/formats/jina/mod.rs
Normal file
2
crates/aether-ai-formats/src/formats/jina/mod.rs
Normal file
@@ -0,0 +1,2 @@
|
||||
pub mod embedding;
|
||||
pub mod rerank;
|
||||
1
crates/aether-ai-formats/src/formats/jina/rerank/mod.rs
Normal file
1
crates/aether-ai-formats/src/formats/jina/rerank/mod.rs
Normal file
@@ -0,0 +1 @@
|
||||
pub mod request;
|
||||
16
crates/aether-ai-formats/src/formats/jina/rerank/request.rs
Normal file
16
crates/aether-ai-formats/src/formats/jina/rerank/request.rs
Normal 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",
|
||||
)
|
||||
}
|
||||
@@ -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)]
|
||||
18
crates/aether-ai-formats/src/formats/mod.rs
Normal file
18
crates/aether-ai-formats/src/formats/mod.rs
Normal 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,
|
||||
};
|
||||
@@ -1,2 +1,3 @@
|
||||
pub mod request;
|
||||
pub mod response;
|
||||
pub mod stream;
|
||||
@@ -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> {
|
||||
@@ -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> {
|
||||
@@ -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 {
|
||||
150
crates/aether-ai-formats/src/formats/openai/embedding/request.rs
Normal file
150
crates/aether-ai-formats/src/formats/openai/embedding/request.rs
Normal 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))])
|
||||
}
|
||||
}
|
||||
@@ -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)
|
||||
}
|
||||
3
crates/aether-ai-formats/src/formats/openai/image/mod.rs
Normal file
3
crates/aether-ai-formats/src/formats/openai/image/mod.rs
Normal file
@@ -0,0 +1,3 @@
|
||||
pub mod request;
|
||||
pub mod spec;
|
||||
pub mod stream;
|
||||
@@ -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,
|
||||
};
|
||||
|
||||
54
crates/aether-ai-formats/src/formats/openai/image/spec.rs
Normal file
54
crates/aether-ai-formats/src/formats/openai/image/spec.rs
Normal 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);
|
||||
}
|
||||
}
|
||||
@@ -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")
|
||||
7
crates/aether-ai-formats/src/formats/openai/mod.rs
Normal file
7
crates/aether-ai-formats/src/formats/openai/mod.rs
Normal 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;
|
||||
@@ -0,0 +1 @@
|
||||
pub mod request;
|
||||
113
crates/aether-ai-formats/src/formats/openai/rerank/request.rs
Normal file
113
crates/aether-ai-formats/src/formats/openai/rerank/request.rs
Normal 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()
|
||||
}
|
||||
}
|
||||
@@ -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(
|
||||
@@ -0,0 +1,5 @@
|
||||
pub mod codex;
|
||||
pub mod request;
|
||||
pub mod response;
|
||||
pub mod spec;
|
||||
pub mod stream;
|
||||
@@ -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))
|
||||
}
|
||||
@@ -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> {
|
||||
@@ -0,0 +1,3 @@
|
||||
pub use crate::formats::openai::chat::stream::{
|
||||
OpenAIResponsesClientEmitter, OpenAIResponsesProviderState,
|
||||
};
|
||||
@@ -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 {
|
||||
1
crates/aether-ai-formats/src/formats/openai/video/mod.rs
Normal file
1
crates/aether-ai-formats/src/formats/openai/video/mod.rs
Normal file
@@ -0,0 +1 @@
|
||||
pub mod spec;
|
||||
27
crates/aether-ai-formats/src/formats/openai/video/spec.rs
Normal file
27
crates/aether-ai-formats/src/formats/openai/video/spec.rs
Normal 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");
|
||||
}
|
||||
}
|
||||
@@ -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}"
|
||||
);
|
||||
}
|
||||
}
|
||||
1043
crates/aether-ai-formats/src/formats/shared/image_bridge.rs
Normal file
1043
crates/aether-ai-formats/src/formats/shared/image_bridge.rs
Normal file
File diff suppressed because it is too large
Load Diff
@@ -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);
|
||||
|
||||
383
crates/aether-ai-formats/src/formats/shared/request.rs
Normal file
383
crates/aether-ai-formats/src/formats/shared/request.rs
Normal 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,
|
||||
));
|
||||
}
|
||||
}
|
||||
@@ -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,
|
||||
};
|
||||
@@ -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(
|
||||
@@ -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>,
|
||||
@@ -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"},
|
||||
@@ -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!({
|
||||
@@ -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 {
|
||||
@@ -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 {
|
||||
@@ -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,
|
||||
@@ -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);
|
||||
};
|
||||
34
crates/aether-ai-formats/src/formats/shared/video.rs
Normal file
34
crates/aether-ai-formats/src/formats/shared/video.rs
Normal 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);
|
||||
}
|
||||
}
|
||||
@@ -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,
|
||||
};
|
||||
|
||||
@@ -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::{
|
||||
|
||||
@@ -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;
|
||||
@@ -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;
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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;
|
||||
|
||||
|
||||
@@ -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};
|
||||
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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"
|
||||
));
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
@@ -1 +0,0 @@
|
||||
pub mod provider;
|
||||
@@ -1,3 +0,0 @@
|
||||
pub mod files;
|
||||
pub mod image;
|
||||
pub mod video;
|
||||
@@ -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
Reference in New Issue
Block a user