mirror of
https://github.com/fawney19/Aether.git
synced 2026-09-02 09:20:22 +08:00
refactor: fold ai surfaces into formats
This commit is contained in:
80
crates/aether-ai-formats/src/request/common.rs
Normal file
80
crates/aether-ai-formats/src/request/common.rs
Normal file
@@ -0,0 +1,80 @@
|
||||
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
crates/aether-ai-formats/src/request/matrix.rs
Normal file
1
crates/aether-ai-formats/src/request/matrix.rs
Normal file
@@ -0,0 +1 @@
|
||||
pub use crate::request::standard::matrix::build_standard_request_body_from_canonical;
|
||||
7
crates/aether-ai-formats/src/request/mod.rs
Normal file
7
crates/aether-ai-formats/src/request/mod.rs
Normal file
@@ -0,0 +1,7 @@
|
||||
pub mod common;
|
||||
pub mod matrix;
|
||||
pub mod openai;
|
||||
pub mod passthrough;
|
||||
pub mod route;
|
||||
pub mod specialized;
|
||||
pub mod standard;
|
||||
104
crates/aether-ai-formats/src/request/openai.rs
Normal file
104
crates/aether-ai-formats/src/request/openai.rs
Normal file
@@ -0,0 +1,104 @@
|
||||
use serde_json::{Map, Value};
|
||||
|
||||
pub fn parse_openai_stop_sequences(stop: Option<&Value>) -> Option<Vec<Value>> {
|
||||
match stop {
|
||||
Some(Value::String(value)) if !value.trim().is_empty() => {
|
||||
Some(vec![Value::String(value.clone())])
|
||||
}
|
||||
Some(Value::Array(values)) => Some(
|
||||
values
|
||||
.iter()
|
||||
.filter_map(|value| value.as_str())
|
||||
.map(str::trim)
|
||||
.filter(|value| !value.is_empty())
|
||||
.map(|value| Value::String(value.to_string()))
|
||||
.collect::<Vec<_>>(),
|
||||
)
|
||||
.filter(|values| !values.is_empty()),
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn resolve_openai_chat_max_tokens(request: &Map<String, Value>) -> u64 {
|
||||
request
|
||||
.get("max_completion_tokens")
|
||||
.and_then(value_as_u64)
|
||||
.or_else(|| request.get("max_tokens").and_then(value_as_u64))
|
||||
.unwrap_or(4096)
|
||||
}
|
||||
|
||||
pub fn value_as_u64(value: &Value) -> Option<u64> {
|
||||
value
|
||||
.as_u64()
|
||||
.or_else(|| value.as_i64().and_then(|value| u64::try_from(value).ok()))
|
||||
}
|
||||
|
||||
pub fn copy_request_number_field(
|
||||
request: &Map<String, Value>,
|
||||
target: &mut Map<String, Value>,
|
||||
key: &str,
|
||||
) {
|
||||
copy_request_number_field_as(request, target, key, key);
|
||||
}
|
||||
|
||||
pub fn copy_request_number_field_as(
|
||||
request: &Map<String, Value>,
|
||||
target: &mut Map<String, Value>,
|
||||
source_key: &str,
|
||||
target_key: &str,
|
||||
) {
|
||||
if let Some(value) = request.get(source_key).cloned() {
|
||||
if value.is_number() {
|
||||
target.insert(target_key.to_string(), value);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub fn map_openai_reasoning_effort_to_claude_output(value: &str) -> Option<&'static str> {
|
||||
match value.trim().to_ascii_lowercase().as_str() {
|
||||
"low" => Some("low"),
|
||||
"medium" => Some("medium"),
|
||||
"high" => Some("high"),
|
||||
"xhigh" => Some("max"),
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn map_openai_reasoning_effort_to_thinking_budget(value: &str) -> Option<u64> {
|
||||
match value.trim().to_ascii_lowercase().as_str() {
|
||||
"low" => Some(1280),
|
||||
"medium" => Some(2048),
|
||||
"high" => Some(4096),
|
||||
"xhigh" => Some(8192),
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn map_openai_reasoning_effort_to_gemini_budget(value: &str) -> Option<u64> {
|
||||
map_openai_reasoning_effort_to_thinking_budget(value)
|
||||
}
|
||||
|
||||
pub fn map_thinking_budget_to_openai_reasoning_effort(value: u64) -> &'static str {
|
||||
match value {
|
||||
0..=1664 => "low",
|
||||
1665..=3072 => "medium",
|
||||
3073..=6144 => "high",
|
||||
_ => "xhigh",
|
||||
}
|
||||
}
|
||||
|
||||
pub fn extract_openai_reasoning_effort(request: &Map<String, Value>) -> Option<String> {
|
||||
request
|
||||
.get("reasoning_effort")
|
||||
.and_then(Value::as_str)
|
||||
.or_else(|| {
|
||||
request
|
||||
.get("reasoning")
|
||||
.and_then(Value::as_object)
|
||||
.and_then(|reasoning| reasoning.get("effort"))
|
||||
.and_then(Value::as_str)
|
||||
})
|
||||
.map(str::trim)
|
||||
.filter(|value| !value.is_empty())
|
||||
.map(|value| value.to_ascii_lowercase())
|
||||
}
|
||||
1
crates/aether-ai-formats/src/request/passthrough/mod.rs
Normal file
1
crates/aether-ai-formats/src/request/passthrough/mod.rs
Normal file
@@ -0,0 +1 @@
|
||||
pub mod provider;
|
||||
109
crates/aether-ai-formats/src/request/passthrough/provider.rs
Normal file
109
crates/aether-ai-formats/src/request/passthrough/provider.rs
Normal file
@@ -0,0 +1,109 @@
|
||||
use crate::contracts::{
|
||||
CLAUDE_CHAT_STREAM_PLAN_KIND, CLAUDE_CHAT_SYNC_PLAN_KIND, CLAUDE_CLI_STREAM_PLAN_KIND,
|
||||
CLAUDE_CLI_SYNC_PLAN_KIND, GEMINI_CHAT_STREAM_PLAN_KIND, GEMINI_CHAT_SYNC_PLAN_KIND,
|
||||
GEMINI_CLI_STREAM_PLAN_KIND, GEMINI_CLI_SYNC_PLAN_KIND,
|
||||
};
|
||||
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
||||
pub enum LocalSameFormatProviderFamily {
|
||||
Standard,
|
||||
Gemini,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Copy)]
|
||||
pub struct LocalSameFormatProviderSpec {
|
||||
pub api_format: &'static str,
|
||||
pub decision_kind: &'static str,
|
||||
pub report_kind: &'static str,
|
||||
pub family: LocalSameFormatProviderFamily,
|
||||
pub require_streaming: bool,
|
||||
}
|
||||
|
||||
pub fn resolve_sync_spec(plan_kind: &str) -> Option<LocalSameFormatProviderSpec> {
|
||||
match plan_kind {
|
||||
CLAUDE_CHAT_SYNC_PLAN_KIND => Some(LocalSameFormatProviderSpec {
|
||||
api_format: "claude:messages",
|
||||
decision_kind: CLAUDE_CHAT_SYNC_PLAN_KIND,
|
||||
report_kind: "claude_chat_sync_success",
|
||||
family: LocalSameFormatProviderFamily::Standard,
|
||||
require_streaming: false,
|
||||
}),
|
||||
CLAUDE_CLI_SYNC_PLAN_KIND => Some(LocalSameFormatProviderSpec {
|
||||
api_format: "claude:messages",
|
||||
decision_kind: CLAUDE_CLI_SYNC_PLAN_KIND,
|
||||
report_kind: "claude_cli_sync_success",
|
||||
family: LocalSameFormatProviderFamily::Standard,
|
||||
require_streaming: false,
|
||||
}),
|
||||
GEMINI_CHAT_SYNC_PLAN_KIND => Some(LocalSameFormatProviderSpec {
|
||||
api_format: "gemini:generate_content",
|
||||
decision_kind: GEMINI_CHAT_SYNC_PLAN_KIND,
|
||||
report_kind: "gemini_chat_sync_success",
|
||||
family: LocalSameFormatProviderFamily::Gemini,
|
||||
require_streaming: false,
|
||||
}),
|
||||
GEMINI_CLI_SYNC_PLAN_KIND => Some(LocalSameFormatProviderSpec {
|
||||
api_format: "gemini:generate_content",
|
||||
decision_kind: GEMINI_CLI_SYNC_PLAN_KIND,
|
||||
report_kind: "gemini_cli_sync_success",
|
||||
family: LocalSameFormatProviderFamily::Gemini,
|
||||
require_streaming: false,
|
||||
}),
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn resolve_stream_spec(plan_kind: &str) -> Option<LocalSameFormatProviderSpec> {
|
||||
match plan_kind {
|
||||
CLAUDE_CHAT_STREAM_PLAN_KIND => Some(LocalSameFormatProviderSpec {
|
||||
api_format: "claude:messages",
|
||||
decision_kind: CLAUDE_CHAT_STREAM_PLAN_KIND,
|
||||
report_kind: "claude_chat_stream_success",
|
||||
family: LocalSameFormatProviderFamily::Standard,
|
||||
require_streaming: true,
|
||||
}),
|
||||
CLAUDE_CLI_STREAM_PLAN_KIND => Some(LocalSameFormatProviderSpec {
|
||||
api_format: "claude:messages",
|
||||
decision_kind: CLAUDE_CLI_STREAM_PLAN_KIND,
|
||||
report_kind: "claude_cli_stream_success",
|
||||
family: LocalSameFormatProviderFamily::Standard,
|
||||
require_streaming: true,
|
||||
}),
|
||||
GEMINI_CHAT_STREAM_PLAN_KIND => Some(LocalSameFormatProviderSpec {
|
||||
api_format: "gemini:generate_content",
|
||||
decision_kind: GEMINI_CHAT_STREAM_PLAN_KIND,
|
||||
report_kind: "gemini_chat_stream_success",
|
||||
family: LocalSameFormatProviderFamily::Gemini,
|
||||
require_streaming: true,
|
||||
}),
|
||||
GEMINI_CLI_STREAM_PLAN_KIND => Some(LocalSameFormatProviderSpec {
|
||||
api_format: "gemini:generate_content",
|
||||
decision_kind: GEMINI_CLI_STREAM_PLAN_KIND,
|
||||
report_kind: "gemini_cli_stream_success",
|
||||
family: LocalSameFormatProviderFamily::Gemini,
|
||||
require_streaming: true,
|
||||
}),
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::{resolve_stream_spec, resolve_sync_spec};
|
||||
|
||||
#[test]
|
||||
fn resolves_claude_sync_same_format_spec() {
|
||||
let spec = resolve_sync_spec("claude_chat_sync").expect("spec");
|
||||
assert_eq!(spec.api_format, "claude:messages");
|
||||
assert_eq!(spec.report_kind, "claude_chat_sync_success");
|
||||
assert!(!spec.require_streaming);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn resolves_gemini_stream_same_format_spec() {
|
||||
let spec = resolve_stream_spec("gemini_cli_stream").expect("spec");
|
||||
assert_eq!(spec.api_format, "gemini:generate_content");
|
||||
assert_eq!(spec.report_kind, "gemini_cli_stream_success");
|
||||
assert!(spec.require_streaming);
|
||||
}
|
||||
}
|
||||
602
crates/aether-ai-formats/src/request/route.rs
Normal file
602
crates/aether-ai-formats/src/request/route.rs
Normal file
@@ -0,0 +1,602 @@
|
||||
use http::Method;
|
||||
|
||||
use crate::contracts::{
|
||||
CLAUDE_CHAT_STREAM_PLAN_KIND, CLAUDE_CHAT_SYNC_PLAN_KIND, CLAUDE_CLI_STREAM_PLAN_KIND,
|
||||
CLAUDE_CLI_SYNC_PLAN_KIND, GEMINI_CHAT_STREAM_PLAN_KIND, GEMINI_CHAT_SYNC_PLAN_KIND,
|
||||
GEMINI_CLI_STREAM_PLAN_KIND, GEMINI_CLI_SYNC_PLAN_KIND, GEMINI_FILES_DELETE_PLAN_KIND,
|
||||
GEMINI_FILES_DOWNLOAD_PLAN_KIND, GEMINI_FILES_GET_PLAN_KIND, GEMINI_FILES_LIST_PLAN_KIND,
|
||||
GEMINI_FILES_UPLOAD_PLAN_KIND, GEMINI_VIDEO_CANCEL_SYNC_PLAN_KIND,
|
||||
GEMINI_VIDEO_CREATE_SYNC_PLAN_KIND, OPENAI_CHAT_STREAM_PLAN_KIND, OPENAI_CHAT_SYNC_PLAN_KIND,
|
||||
OPENAI_IMAGE_STREAM_PLAN_KIND, OPENAI_IMAGE_SYNC_PLAN_KIND,
|
||||
OPENAI_RESPONSES_COMPACT_STREAM_PLAN_KIND, OPENAI_RESPONSES_COMPACT_SYNC_PLAN_KIND,
|
||||
OPENAI_RESPONSES_STREAM_PLAN_KIND, OPENAI_RESPONSES_SYNC_PLAN_KIND,
|
||||
OPENAI_VIDEO_CANCEL_SYNC_PLAN_KIND, OPENAI_VIDEO_CONTENT_PLAN_KIND,
|
||||
OPENAI_VIDEO_CREATE_SYNC_PLAN_KIND, OPENAI_VIDEO_DELETE_SYNC_PLAN_KIND,
|
||||
OPENAI_VIDEO_REMIX_SYNC_PLAN_KIND,
|
||||
};
|
||||
use crate::request::specialized::image::is_openai_image_stream_request;
|
||||
|
||||
pub fn resolve_execution_runtime_stream_plan_kind(
|
||||
route_class: Option<&str>,
|
||||
route_family: Option<&str>,
|
||||
route_kind: Option<&str>,
|
||||
method: &Method,
|
||||
path: &str,
|
||||
) -> Option<&'static str> {
|
||||
if route_class != Some("ai_public") {
|
||||
return None;
|
||||
}
|
||||
|
||||
if route_family == Some("gemini")
|
||||
&& route_kind == Some("files")
|
||||
&& *method == Method::GET
|
||||
&& path.ends_with(":download")
|
||||
{
|
||||
return Some(GEMINI_FILES_DOWNLOAD_PLAN_KIND);
|
||||
}
|
||||
|
||||
if route_family == Some("openai")
|
||||
&& route_kind == Some("chat")
|
||||
&& *method == Method::POST
|
||||
&& path == "/v1/chat/completions"
|
||||
{
|
||||
return Some(OPENAI_CHAT_STREAM_PLAN_KIND);
|
||||
}
|
||||
|
||||
if route_family == Some("claude")
|
||||
&& is_claude_messages_route_kind(route_kind)
|
||||
&& *method == Method::POST
|
||||
&& path == "/v1/messages"
|
||||
{
|
||||
return Some(CLAUDE_CHAT_STREAM_PLAN_KIND);
|
||||
}
|
||||
|
||||
if route_family == Some("claude")
|
||||
&& route_kind == Some("cli")
|
||||
&& *method == Method::POST
|
||||
&& path == "/v1/messages"
|
||||
{
|
||||
return Some(CLAUDE_CLI_STREAM_PLAN_KIND);
|
||||
}
|
||||
|
||||
if route_family == Some("gemini")
|
||||
&& is_gemini_generate_content_route_kind(route_kind)
|
||||
&& *method == Method::POST
|
||||
&& path.ends_with(":streamGenerateContent")
|
||||
{
|
||||
return Some(GEMINI_CHAT_STREAM_PLAN_KIND);
|
||||
}
|
||||
|
||||
if route_family == Some("gemini")
|
||||
&& route_kind == Some("cli")
|
||||
&& *method == Method::POST
|
||||
&& path.ends_with(":streamGenerateContent")
|
||||
{
|
||||
return Some(GEMINI_CLI_STREAM_PLAN_KIND);
|
||||
}
|
||||
|
||||
if route_family == Some("openai")
|
||||
&& is_openai_responses_route_kind(route_kind)
|
||||
&& *method == Method::POST
|
||||
&& path == "/v1/responses"
|
||||
{
|
||||
return Some(OPENAI_RESPONSES_STREAM_PLAN_KIND);
|
||||
}
|
||||
|
||||
if route_family == Some("openai")
|
||||
&& is_openai_responses_compact_route_kind(route_kind)
|
||||
&& *method == Method::POST
|
||||
&& path == "/v1/responses/compact"
|
||||
{
|
||||
return Some(OPENAI_RESPONSES_COMPACT_STREAM_PLAN_KIND);
|
||||
}
|
||||
|
||||
if route_family == Some("openai")
|
||||
&& route_kind == Some("image")
|
||||
&& *method == Method::POST
|
||||
&& matches!(path, "/v1/images/generations" | "/v1/images/edits")
|
||||
{
|
||||
return Some(OPENAI_IMAGE_STREAM_PLAN_KIND);
|
||||
}
|
||||
|
||||
if route_family == Some("openai")
|
||||
&& route_kind == Some("video")
|
||||
&& *method == Method::GET
|
||||
&& path.ends_with("/content")
|
||||
{
|
||||
return Some(OPENAI_VIDEO_CONTENT_PLAN_KIND);
|
||||
}
|
||||
|
||||
None
|
||||
}
|
||||
|
||||
pub fn resolve_execution_runtime_sync_plan_kind(
|
||||
route_class: Option<&str>,
|
||||
route_family: Option<&str>,
|
||||
route_kind: Option<&str>,
|
||||
method: &Method,
|
||||
path: &str,
|
||||
) -> Option<&'static str> {
|
||||
if route_class != Some("ai_public") {
|
||||
return None;
|
||||
}
|
||||
|
||||
if route_family == Some("openai")
|
||||
&& route_kind == Some("video")
|
||||
&& *method == Method::POST
|
||||
&& path.starts_with("/v1/videos/")
|
||||
&& path.ends_with("/cancel")
|
||||
{
|
||||
return Some(OPENAI_VIDEO_CANCEL_SYNC_PLAN_KIND);
|
||||
}
|
||||
|
||||
if route_family == Some("openai")
|
||||
&& route_kind == Some("video")
|
||||
&& *method == Method::POST
|
||||
&& path.starts_with("/v1/videos/")
|
||||
&& path.ends_with("/remix")
|
||||
{
|
||||
return Some(OPENAI_VIDEO_REMIX_SYNC_PLAN_KIND);
|
||||
}
|
||||
|
||||
if route_family == Some("openai")
|
||||
&& route_kind == Some("video")
|
||||
&& *method == Method::POST
|
||||
&& path == "/v1/videos"
|
||||
{
|
||||
return Some(OPENAI_VIDEO_CREATE_SYNC_PLAN_KIND);
|
||||
}
|
||||
|
||||
if route_family == Some("openai")
|
||||
&& route_kind == Some("video")
|
||||
&& *method == Method::DELETE
|
||||
&& path.starts_with("/v1/videos/")
|
||||
{
|
||||
return Some(OPENAI_VIDEO_DELETE_SYNC_PLAN_KIND);
|
||||
}
|
||||
|
||||
if route_family == Some("gemini")
|
||||
&& route_kind == Some("video")
|
||||
&& *method == Method::POST
|
||||
&& path.ends_with(":cancel")
|
||||
{
|
||||
return Some(GEMINI_VIDEO_CANCEL_SYNC_PLAN_KIND);
|
||||
}
|
||||
|
||||
if route_family == Some("gemini")
|
||||
&& route_kind == Some("video")
|
||||
&& *method == Method::POST
|
||||
&& path.ends_with(":predictLongRunning")
|
||||
{
|
||||
return Some(GEMINI_VIDEO_CREATE_SYNC_PLAN_KIND);
|
||||
}
|
||||
|
||||
if route_family == Some("openai")
|
||||
&& route_kind == Some("chat")
|
||||
&& *method == Method::POST
|
||||
&& path == "/v1/chat/completions"
|
||||
{
|
||||
return Some(OPENAI_CHAT_SYNC_PLAN_KIND);
|
||||
}
|
||||
|
||||
if route_family == Some("openai")
|
||||
&& route_kind == Some("image")
|
||||
&& *method == Method::POST
|
||||
&& matches!(
|
||||
path,
|
||||
"/v1/images/generations" | "/v1/images/edits" | "/v1/images/variations"
|
||||
)
|
||||
{
|
||||
return Some(OPENAI_IMAGE_SYNC_PLAN_KIND);
|
||||
}
|
||||
|
||||
if route_family == Some("openai")
|
||||
&& is_openai_responses_route_kind(route_kind)
|
||||
&& *method == Method::POST
|
||||
&& path == "/v1/responses"
|
||||
{
|
||||
return Some(OPENAI_RESPONSES_SYNC_PLAN_KIND);
|
||||
}
|
||||
|
||||
if route_family == Some("openai")
|
||||
&& is_openai_responses_compact_route_kind(route_kind)
|
||||
&& *method == Method::POST
|
||||
&& path == "/v1/responses/compact"
|
||||
{
|
||||
return Some(OPENAI_RESPONSES_COMPACT_SYNC_PLAN_KIND);
|
||||
}
|
||||
|
||||
if route_family == Some("claude")
|
||||
&& is_claude_messages_route_kind(route_kind)
|
||||
&& *method == Method::POST
|
||||
&& path == "/v1/messages"
|
||||
{
|
||||
return Some(CLAUDE_CHAT_SYNC_PLAN_KIND);
|
||||
}
|
||||
|
||||
if route_family == Some("claude")
|
||||
&& route_kind == Some("cli")
|
||||
&& *method == Method::POST
|
||||
&& path == "/v1/messages"
|
||||
{
|
||||
return Some(CLAUDE_CLI_SYNC_PLAN_KIND);
|
||||
}
|
||||
|
||||
if route_family == Some("gemini")
|
||||
&& is_gemini_generate_content_route_kind(route_kind)
|
||||
&& *method == Method::POST
|
||||
&& path.ends_with(":generateContent")
|
||||
{
|
||||
return Some(GEMINI_CHAT_SYNC_PLAN_KIND);
|
||||
}
|
||||
|
||||
if route_family == Some("gemini")
|
||||
&& route_kind == Some("cli")
|
||||
&& *method == Method::POST
|
||||
&& path.ends_with(":generateContent")
|
||||
{
|
||||
return Some(GEMINI_CLI_SYNC_PLAN_KIND);
|
||||
}
|
||||
|
||||
if route_family == Some("gemini") && route_kind == Some("files") {
|
||||
if *method == Method::POST && path == "/upload/v1beta/files" {
|
||||
return Some(GEMINI_FILES_UPLOAD_PLAN_KIND);
|
||||
}
|
||||
if *method == Method::GET && path == "/v1beta/files" {
|
||||
return Some(GEMINI_FILES_LIST_PLAN_KIND);
|
||||
}
|
||||
if *method == Method::GET
|
||||
&& path.starts_with("/v1beta/files/")
|
||||
&& !path.ends_with(":download")
|
||||
{
|
||||
return Some(GEMINI_FILES_GET_PLAN_KIND);
|
||||
}
|
||||
if *method == Method::DELETE
|
||||
&& path.starts_with("/v1beta/files/")
|
||||
&& !path.ends_with(":download")
|
||||
{
|
||||
return Some(GEMINI_FILES_DELETE_PLAN_KIND);
|
||||
}
|
||||
}
|
||||
|
||||
None
|
||||
}
|
||||
|
||||
fn is_openai_responses_route_kind(route_kind: Option<&str>) -> bool {
|
||||
matches!(route_kind, Some("responses") | Some("cli"))
|
||||
}
|
||||
|
||||
fn is_openai_responses_compact_route_kind(route_kind: Option<&str>) -> bool {
|
||||
matches!(route_kind, Some("responses:compact") | Some("compact"))
|
||||
}
|
||||
|
||||
fn is_claude_messages_route_kind(route_kind: Option<&str>) -> bool {
|
||||
matches!(route_kind, Some("messages") | Some("chat"))
|
||||
}
|
||||
|
||||
fn is_gemini_generate_content_route_kind(route_kind: Option<&str>) -> bool {
|
||||
matches!(route_kind, Some("generate_content") | Some("chat"))
|
||||
}
|
||||
|
||||
pub fn is_matching_stream_request(
|
||||
plan_kind: &str,
|
||||
path: &str,
|
||||
body_json: &serde_json::Value,
|
||||
) -> bool {
|
||||
match plan_kind {
|
||||
OPENAI_CHAT_STREAM_PLAN_KIND
|
||||
| CLAUDE_CHAT_STREAM_PLAN_KIND
|
||||
| OPENAI_RESPONSES_STREAM_PLAN_KIND
|
||||
| OPENAI_RESPONSES_COMPACT_STREAM_PLAN_KIND
|
||||
| CLAUDE_CLI_STREAM_PLAN_KIND
|
||||
| OPENAI_IMAGE_STREAM_PLAN_KIND => body_json
|
||||
.get("stream")
|
||||
.and_then(|value| value.as_bool())
|
||||
.unwrap_or(false),
|
||||
GEMINI_CHAT_STREAM_PLAN_KIND | GEMINI_CLI_STREAM_PLAN_KIND => {
|
||||
path.ends_with(":streamGenerateContent")
|
||||
}
|
||||
_ => true,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn is_matching_stream_http_request(
|
||||
plan_kind: &str,
|
||||
parts: &http::request::Parts,
|
||||
body_json: &serde_json::Value,
|
||||
body_base64: Option<&str>,
|
||||
) -> bool {
|
||||
if plan_kind == OPENAI_IMAGE_STREAM_PLAN_KIND {
|
||||
return is_openai_image_stream_request(parts, body_json, body_base64);
|
||||
}
|
||||
|
||||
is_matching_stream_request(plan_kind, parts.uri.path(), body_json)
|
||||
}
|
||||
|
||||
pub fn supports_sync_execution_decision_kind(plan_kind: &str) -> bool {
|
||||
matches!(
|
||||
plan_kind,
|
||||
OPENAI_CHAT_SYNC_PLAN_KIND
|
||||
| OPENAI_IMAGE_SYNC_PLAN_KIND
|
||||
| OPENAI_RESPONSES_SYNC_PLAN_KIND
|
||||
| OPENAI_RESPONSES_COMPACT_SYNC_PLAN_KIND
|
||||
| CLAUDE_CHAT_SYNC_PLAN_KIND
|
||||
| CLAUDE_CLI_SYNC_PLAN_KIND
|
||||
| GEMINI_CHAT_SYNC_PLAN_KIND
|
||||
| GEMINI_CLI_SYNC_PLAN_KIND
|
||||
| GEMINI_FILES_UPLOAD_PLAN_KIND
|
||||
| OPENAI_VIDEO_CREATE_SYNC_PLAN_KIND
|
||||
| OPENAI_VIDEO_REMIX_SYNC_PLAN_KIND
|
||||
| OPENAI_VIDEO_CANCEL_SYNC_PLAN_KIND
|
||||
| OPENAI_VIDEO_DELETE_SYNC_PLAN_KIND
|
||||
| GEMINI_VIDEO_CREATE_SYNC_PLAN_KIND
|
||||
| GEMINI_VIDEO_CANCEL_SYNC_PLAN_KIND
|
||||
| GEMINI_FILES_GET_PLAN_KIND
|
||||
| GEMINI_FILES_LIST_PLAN_KIND
|
||||
| GEMINI_FILES_DELETE_PLAN_KIND
|
||||
)
|
||||
}
|
||||
|
||||
pub fn supports_stream_execution_decision_kind(plan_kind: &str) -> bool {
|
||||
matches!(
|
||||
plan_kind,
|
||||
OPENAI_CHAT_STREAM_PLAN_KIND
|
||||
| CLAUDE_CHAT_STREAM_PLAN_KIND
|
||||
| GEMINI_CHAT_STREAM_PLAN_KIND
|
||||
| OPENAI_RESPONSES_STREAM_PLAN_KIND
|
||||
| OPENAI_RESPONSES_COMPACT_STREAM_PLAN_KIND
|
||||
| OPENAI_IMAGE_STREAM_PLAN_KIND
|
||||
| CLAUDE_CLI_STREAM_PLAN_KIND
|
||||
| GEMINI_CLI_STREAM_PLAN_KIND
|
||||
| GEMINI_FILES_DOWNLOAD_PLAN_KIND
|
||||
| OPENAI_VIDEO_CONTENT_PLAN_KIND
|
||||
)
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use base64::Engine as _;
|
||||
use http::Method;
|
||||
|
||||
use super::{
|
||||
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,
|
||||
};
|
||||
use crate::contracts::{
|
||||
OPENAI_CHAT_STREAM_PLAN_KIND, OPENAI_CHAT_SYNC_PLAN_KIND, OPENAI_IMAGE_STREAM_PLAN_KIND,
|
||||
OPENAI_IMAGE_SYNC_PLAN_KIND, OPENAI_RESPONSES_COMPACT_STREAM_PLAN_KIND,
|
||||
OPENAI_RESPONSES_COMPACT_SYNC_PLAN_KIND, OPENAI_RESPONSES_STREAM_PLAN_KIND,
|
||||
OPENAI_RESPONSES_SYNC_PLAN_KIND,
|
||||
};
|
||||
|
||||
#[test]
|
||||
fn resolves_openai_chat_plan_kinds() {
|
||||
assert_eq!(
|
||||
resolve_execution_runtime_sync_plan_kind(
|
||||
Some("ai_public"),
|
||||
Some("openai"),
|
||||
Some("chat"),
|
||||
&Method::POST,
|
||||
"/v1/chat/completions",
|
||||
),
|
||||
Some(OPENAI_CHAT_SYNC_PLAN_KIND)
|
||||
);
|
||||
assert_eq!(
|
||||
resolve_execution_runtime_stream_plan_kind(
|
||||
Some("ai_public"),
|
||||
Some("openai"),
|
||||
Some("chat"),
|
||||
&Method::POST,
|
||||
"/v1/chat/completions",
|
||||
),
|
||||
Some(OPENAI_CHAT_STREAM_PLAN_KIND)
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn resolves_openai_responses_plan_kinds() {
|
||||
assert_eq!(
|
||||
resolve_execution_runtime_sync_plan_kind(
|
||||
Some("ai_public"),
|
||||
Some("openai"),
|
||||
Some("responses"),
|
||||
&Method::POST,
|
||||
"/v1/responses",
|
||||
),
|
||||
Some(OPENAI_RESPONSES_SYNC_PLAN_KIND)
|
||||
);
|
||||
assert_eq!(
|
||||
resolve_execution_runtime_stream_plan_kind(
|
||||
Some("ai_public"),
|
||||
Some("openai"),
|
||||
Some("responses"),
|
||||
&Method::POST,
|
||||
"/v1/responses",
|
||||
),
|
||||
Some(OPENAI_RESPONSES_STREAM_PLAN_KIND)
|
||||
);
|
||||
assert_eq!(
|
||||
resolve_execution_runtime_sync_plan_kind(
|
||||
Some("ai_public"),
|
||||
Some("openai"),
|
||||
Some("cli"),
|
||||
&Method::POST,
|
||||
"/v1/responses",
|
||||
),
|
||||
Some(OPENAI_RESPONSES_SYNC_PLAN_KIND)
|
||||
);
|
||||
assert!(supports_sync_execution_decision_kind(
|
||||
OPENAI_RESPONSES_SYNC_PLAN_KIND
|
||||
));
|
||||
assert!(supports_stream_execution_decision_kind(
|
||||
OPENAI_RESPONSES_STREAM_PLAN_KIND
|
||||
));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn resolves_openai_responses_compact_plan_kinds() {
|
||||
assert_eq!(
|
||||
resolve_execution_runtime_sync_plan_kind(
|
||||
Some("ai_public"),
|
||||
Some("openai"),
|
||||
Some("responses:compact"),
|
||||
&Method::POST,
|
||||
"/v1/responses/compact",
|
||||
),
|
||||
Some(OPENAI_RESPONSES_COMPACT_SYNC_PLAN_KIND)
|
||||
);
|
||||
assert_eq!(
|
||||
resolve_execution_runtime_stream_plan_kind(
|
||||
Some("ai_public"),
|
||||
Some("openai"),
|
||||
Some("responses:compact"),
|
||||
&Method::POST,
|
||||
"/v1/responses/compact",
|
||||
),
|
||||
Some(OPENAI_RESPONSES_COMPACT_STREAM_PLAN_KIND)
|
||||
);
|
||||
assert_eq!(
|
||||
resolve_execution_runtime_sync_plan_kind(
|
||||
Some("ai_public"),
|
||||
Some("openai"),
|
||||
Some("compact"),
|
||||
&Method::POST,
|
||||
"/v1/responses/compact",
|
||||
),
|
||||
Some(OPENAI_RESPONSES_COMPACT_SYNC_PLAN_KIND)
|
||||
);
|
||||
assert!(supports_sync_execution_decision_kind(
|
||||
OPENAI_RESPONSES_COMPACT_SYNC_PLAN_KIND
|
||||
));
|
||||
assert!(supports_stream_execution_decision_kind(
|
||||
OPENAI_RESPONSES_COMPACT_STREAM_PLAN_KIND
|
||||
));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn stream_matching_requires_openai_stream_flag() {
|
||||
assert!(!is_matching_stream_request(
|
||||
OPENAI_CHAT_STREAM_PLAN_KIND,
|
||||
"/v1/chat/completions",
|
||||
&serde_json::json!({"stream": false}),
|
||||
));
|
||||
assert!(is_matching_stream_request(
|
||||
OPENAI_CHAT_STREAM_PLAN_KIND,
|
||||
"/v1/chat/completions",
|
||||
&serde_json::json!({"stream": true}),
|
||||
));
|
||||
assert!(supports_sync_execution_decision_kind(
|
||||
OPENAI_CHAT_SYNC_PLAN_KIND
|
||||
));
|
||||
assert!(supports_stream_execution_decision_kind(
|
||||
OPENAI_CHAT_STREAM_PLAN_KIND
|
||||
));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn resolves_openai_image_sync_plan_kind() {
|
||||
assert_eq!(
|
||||
resolve_execution_runtime_sync_plan_kind(
|
||||
Some("ai_public"),
|
||||
Some("openai"),
|
||||
Some("image"),
|
||||
&Method::POST,
|
||||
"/v1/images/generations",
|
||||
),
|
||||
Some(OPENAI_IMAGE_SYNC_PLAN_KIND)
|
||||
);
|
||||
assert_eq!(
|
||||
resolve_execution_runtime_sync_plan_kind(
|
||||
Some("ai_public"),
|
||||
Some("openai"),
|
||||
Some("image"),
|
||||
&Method::POST,
|
||||
"/v1/images/edits",
|
||||
),
|
||||
Some(OPENAI_IMAGE_SYNC_PLAN_KIND)
|
||||
);
|
||||
assert_eq!(
|
||||
resolve_execution_runtime_sync_plan_kind(
|
||||
Some("ai_public"),
|
||||
Some("openai"),
|
||||
Some("image"),
|
||||
&Method::POST,
|
||||
"/v1/images/variations",
|
||||
),
|
||||
Some(OPENAI_IMAGE_SYNC_PLAN_KIND)
|
||||
);
|
||||
assert!(supports_sync_execution_decision_kind(
|
||||
OPENAI_IMAGE_SYNC_PLAN_KIND
|
||||
));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn resolves_openai_image_stream_plan_kind() {
|
||||
assert_eq!(
|
||||
resolve_execution_runtime_stream_plan_kind(
|
||||
Some("ai_public"),
|
||||
Some("openai"),
|
||||
Some("image"),
|
||||
&Method::POST,
|
||||
"/v1/images/generations",
|
||||
),
|
||||
Some(OPENAI_IMAGE_STREAM_PLAN_KIND)
|
||||
);
|
||||
assert_eq!(
|
||||
resolve_execution_runtime_stream_plan_kind(
|
||||
Some("ai_public"),
|
||||
Some("openai"),
|
||||
Some("image"),
|
||||
&Method::POST,
|
||||
"/v1/images/edits",
|
||||
),
|
||||
Some(OPENAI_IMAGE_STREAM_PLAN_KIND)
|
||||
);
|
||||
assert!(supports_stream_execution_decision_kind(
|
||||
OPENAI_IMAGE_STREAM_PLAN_KIND
|
||||
));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn stream_matching_requires_openai_image_stream_flag() {
|
||||
assert!(!is_matching_stream_request(
|
||||
OPENAI_IMAGE_STREAM_PLAN_KIND,
|
||||
"/v1/images/generations",
|
||||
&serde_json::json!({"stream": false}),
|
||||
));
|
||||
assert!(is_matching_stream_request(
|
||||
OPENAI_IMAGE_STREAM_PLAN_KIND,
|
||||
"/v1/images/generations",
|
||||
&serde_json::json!({"stream": true}),
|
||||
));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn http_stream_matching_detects_openai_image_multipart_stream_flag() {
|
||||
let request = http::Request::builder()
|
||||
.method(Method::POST)
|
||||
.uri("/v1/images/edits")
|
||||
.header(
|
||||
http::header::CONTENT_TYPE,
|
||||
"multipart/form-data; boundary=image-stream-boundary",
|
||||
)
|
||||
.body(())
|
||||
.expect("request should build");
|
||||
let (parts, _) = request.into_parts();
|
||||
let body = concat!(
|
||||
"--image-stream-boundary\r\n",
|
||||
"Content-Disposition: form-data; name=\"stream\"\r\n\r\n",
|
||||
"true\r\n",
|
||||
"--image-stream-boundary--\r\n"
|
||||
);
|
||||
let body_base64 = base64::engine::general_purpose::STANDARD.encode(body.as_bytes());
|
||||
|
||||
assert!(is_matching_stream_http_request(
|
||||
OPENAI_IMAGE_STREAM_PLAN_KIND,
|
||||
&parts,
|
||||
&serde_json::json!({}),
|
||||
Some(body_base64.as_str()),
|
||||
));
|
||||
}
|
||||
}
|
||||
69
crates/aether-ai-formats/src/request/specialized/files.rs
Normal file
69
crates/aether-ai-formats/src/request/specialized/files.rs
Normal file
@@ -0,0 +1,69 @@
|
||||
use crate::contracts::{
|
||||
GEMINI_FILES_DELETE_PLAN_KIND, GEMINI_FILES_DOWNLOAD_PLAN_KIND, GEMINI_FILES_GET_PLAN_KIND,
|
||||
GEMINI_FILES_LIST_PLAN_KIND, GEMINI_FILES_UPLOAD_PLAN_KIND,
|
||||
};
|
||||
|
||||
#[derive(Debug, Clone, Copy)]
|
||||
pub struct LocalGeminiFilesSpec {
|
||||
pub decision_kind: &'static str,
|
||||
pub report_kind: Option<&'static str>,
|
||||
pub require_streaming: bool,
|
||||
}
|
||||
|
||||
pub fn resolve_sync_spec(plan_kind: &str) -> Option<LocalGeminiFilesSpec> {
|
||||
match plan_kind {
|
||||
GEMINI_FILES_UPLOAD_PLAN_KIND => Some(LocalGeminiFilesSpec {
|
||||
decision_kind: GEMINI_FILES_UPLOAD_PLAN_KIND,
|
||||
report_kind: Some("gemini_files_store_mapping"),
|
||||
require_streaming: false,
|
||||
}),
|
||||
GEMINI_FILES_LIST_PLAN_KIND => Some(LocalGeminiFilesSpec {
|
||||
decision_kind: GEMINI_FILES_LIST_PLAN_KIND,
|
||||
report_kind: Some("gemini_files_store_mapping"),
|
||||
require_streaming: false,
|
||||
}),
|
||||
GEMINI_FILES_GET_PLAN_KIND => Some(LocalGeminiFilesSpec {
|
||||
decision_kind: GEMINI_FILES_GET_PLAN_KIND,
|
||||
report_kind: Some("gemini_files_store_mapping"),
|
||||
require_streaming: false,
|
||||
}),
|
||||
GEMINI_FILES_DELETE_PLAN_KIND => Some(LocalGeminiFilesSpec {
|
||||
decision_kind: GEMINI_FILES_DELETE_PLAN_KIND,
|
||||
report_kind: Some("gemini_files_delete_mapping"),
|
||||
require_streaming: false,
|
||||
}),
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn resolve_stream_spec(plan_kind: &str) -> Option<LocalGeminiFilesSpec> {
|
||||
match plan_kind {
|
||||
GEMINI_FILES_DOWNLOAD_PLAN_KIND => Some(LocalGeminiFilesSpec {
|
||||
decision_kind: GEMINI_FILES_DOWNLOAD_PLAN_KIND,
|
||||
report_kind: None,
|
||||
require_streaming: true,
|
||||
}),
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::{resolve_stream_spec, resolve_sync_spec};
|
||||
|
||||
#[test]
|
||||
fn resolves_sync_gemini_files_specs() {
|
||||
let spec = resolve_sync_spec("gemini_files_upload").expect("spec");
|
||||
assert_eq!(spec.decision_kind, "gemini_files_upload");
|
||||
assert_eq!(spec.report_kind, Some("gemini_files_store_mapping"));
|
||||
assert!(!spec.require_streaming);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn resolves_stream_gemini_files_spec() {
|
||||
let spec = resolve_stream_spec("gemini_files_download").expect("spec");
|
||||
assert_eq!(spec.decision_kind, "gemini_files_download");
|
||||
assert_eq!(spec.report_kind, None);
|
||||
assert!(spec.require_streaming);
|
||||
}
|
||||
}
|
||||
1127
crates/aether-ai-formats/src/request/specialized/image.rs
Normal file
1127
crates/aether-ai-formats/src/request/specialized/image.rs
Normal file
File diff suppressed because it is too large
Load Diff
3
crates/aether-ai-formats/src/request/specialized/mod.rs
Normal file
3
crates/aether-ai-formats/src/request/specialized/mod.rs
Normal file
@@ -0,0 +1,3 @@
|
||||
pub mod files;
|
||||
pub mod image;
|
||||
pub mod video;
|
||||
54
crates/aether-ai-formats/src/request/specialized/video.rs
Normal file
54
crates/aether-ai-formats/src/request/specialized/video.rs
Normal file
@@ -0,0 +1,54 @@
|
||||
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");
|
||||
}
|
||||
}
|
||||
53
crates/aether-ai-formats/src/request/standard/claude/chat.rs
Normal file
53
crates/aether-ai-formats/src/request/standard/claude/chat.rs
Normal file
@@ -0,0 +1,53 @@
|
||||
use crate::contracts::{CLAUDE_CHAT_STREAM_PLAN_KIND, CLAUDE_CHAT_SYNC_PLAN_KIND};
|
||||
use crate::request::standard::family::{
|
||||
LocalStandardSourceFamily, LocalStandardSourceMode, LocalStandardSpec,
|
||||
};
|
||||
|
||||
pub fn resolve_sync_spec(plan_kind: &str) -> Option<LocalStandardSpec> {
|
||||
match plan_kind {
|
||||
CLAUDE_CHAT_SYNC_PLAN_KIND => Some(LocalStandardSpec {
|
||||
api_format: "claude:messages",
|
||||
decision_kind: CLAUDE_CHAT_SYNC_PLAN_KIND,
|
||||
report_kind: "claude_chat_sync_finalize",
|
||||
family: LocalStandardSourceFamily::Standard,
|
||||
mode: LocalStandardSourceMode::Chat,
|
||||
require_streaming: false,
|
||||
}),
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn resolve_stream_spec(plan_kind: &str) -> Option<LocalStandardSpec> {
|
||||
match plan_kind {
|
||||
CLAUDE_CHAT_STREAM_PLAN_KIND => Some(LocalStandardSpec {
|
||||
api_format: "claude:messages",
|
||||
decision_kind: CLAUDE_CHAT_STREAM_PLAN_KIND,
|
||||
report_kind: "claude_chat_stream_success",
|
||||
family: LocalStandardSourceFamily::Standard,
|
||||
mode: LocalStandardSourceMode::Chat,
|
||||
require_streaming: true,
|
||||
}),
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::{resolve_stream_spec, resolve_sync_spec};
|
||||
|
||||
#[test]
|
||||
fn resolves_claude_chat_sync_spec() {
|
||||
let spec = resolve_sync_spec("claude_chat_sync").expect("spec");
|
||||
assert_eq!(spec.api_format, "claude:messages");
|
||||
assert_eq!(spec.report_kind, "claude_chat_sync_finalize");
|
||||
assert!(!spec.require_streaming);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn resolves_claude_chat_stream_spec() {
|
||||
let spec = resolve_stream_spec("claude_chat_stream").expect("spec");
|
||||
assert_eq!(spec.api_format, "claude:messages");
|
||||
assert_eq!(spec.report_kind, "claude_chat_stream_success");
|
||||
assert!(spec.require_streaming);
|
||||
}
|
||||
}
|
||||
53
crates/aether-ai-formats/src/request/standard/claude/cli.rs
Normal file
53
crates/aether-ai-formats/src/request/standard/claude/cli.rs
Normal file
@@ -0,0 +1,53 @@
|
||||
use crate::contracts::{CLAUDE_CLI_STREAM_PLAN_KIND, CLAUDE_CLI_SYNC_PLAN_KIND};
|
||||
use crate::request::standard::family::{
|
||||
LocalStandardSourceFamily, LocalStandardSourceMode, LocalStandardSpec,
|
||||
};
|
||||
|
||||
pub fn resolve_sync_spec(plan_kind: &str) -> Option<LocalStandardSpec> {
|
||||
match plan_kind {
|
||||
CLAUDE_CLI_SYNC_PLAN_KIND => Some(LocalStandardSpec {
|
||||
api_format: "claude:messages",
|
||||
decision_kind: CLAUDE_CLI_SYNC_PLAN_KIND,
|
||||
report_kind: "claude_cli_sync_finalize",
|
||||
family: LocalStandardSourceFamily::Standard,
|
||||
mode: LocalStandardSourceMode::Cli,
|
||||
require_streaming: false,
|
||||
}),
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn resolve_stream_spec(plan_kind: &str) -> Option<LocalStandardSpec> {
|
||||
match plan_kind {
|
||||
CLAUDE_CLI_STREAM_PLAN_KIND => Some(LocalStandardSpec {
|
||||
api_format: "claude:messages",
|
||||
decision_kind: CLAUDE_CLI_STREAM_PLAN_KIND,
|
||||
report_kind: "claude_cli_stream_success",
|
||||
family: LocalStandardSourceFamily::Standard,
|
||||
mode: LocalStandardSourceMode::Cli,
|
||||
require_streaming: true,
|
||||
}),
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::{resolve_stream_spec, resolve_sync_spec};
|
||||
|
||||
#[test]
|
||||
fn resolves_claude_cli_sync_spec() {
|
||||
let spec = resolve_sync_spec("claude_cli_sync").expect("spec");
|
||||
assert_eq!(spec.api_format, "claude:messages");
|
||||
assert_eq!(spec.report_kind, "claude_cli_sync_finalize");
|
||||
assert!(!spec.require_streaming);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn resolves_claude_cli_stream_spec() {
|
||||
let spec = resolve_stream_spec("claude_cli_stream").expect("spec");
|
||||
assert_eq!(spec.api_format, "claude:messages");
|
||||
assert_eq!(spec.report_kind, "claude_cli_stream_success");
|
||||
assert!(spec.require_streaming);
|
||||
}
|
||||
}
|
||||
12
crates/aether-ai-formats/src/request/standard/claude/mod.rs
Normal file
12
crates/aether-ai-formats/src/request/standard/claude/mod.rs
Normal file
@@ -0,0 +1,12 @@
|
||||
pub mod chat;
|
||||
pub mod cli;
|
||||
|
||||
use crate::request::standard::LocalStandardSpec;
|
||||
|
||||
pub fn resolve_sync_spec(plan_kind: &str) -> Option<LocalStandardSpec> {
|
||||
chat::resolve_sync_spec(plan_kind).or_else(|| cli::resolve_sync_spec(plan_kind))
|
||||
}
|
||||
|
||||
pub fn resolve_stream_spec(plan_kind: &str) -> Option<LocalStandardSpec> {
|
||||
chat::resolve_stream_spec(plan_kind).or_else(|| cli::resolve_stream_spec(plan_kind))
|
||||
}
|
||||
519
crates/aether-ai-formats/src/request/standard/codex.rs
Normal file
519
crates/aether-ai-formats/src/request/standard/codex.rs
Normal file
@@ -0,0 +1,519 @@
|
||||
use std::collections::BTreeMap;
|
||||
use std::fmt::Write;
|
||||
|
||||
use aether_ai_formats::provider_compat::proxy::rules::body_rules_handle_path;
|
||||
use serde_json::{json, Value};
|
||||
use sha1::{Digest as Sha1Digest, Sha1};
|
||||
use sha2::Sha256;
|
||||
use uuid::Uuid;
|
||||
|
||||
const CODEX_PROMPT_CACHE_NAMESPACE_VERSION: &str = "v3";
|
||||
const CODEX_DEFAULT_INSTRUCTIONS: &str = "You are ChatGPT.";
|
||||
const CODEX_DEFAULT_USER_AGENT: &str =
|
||||
"codex-tui/0.122.0 (Mac OS 15.2.0; arm64) vscode/2.6.11 (codex-tui; 0.122.0)";
|
||||
const CODEX_DEFAULT_ORIGINATOR: &str = "codex-tui";
|
||||
pub const CODEX_OPENAI_IMAGE_INTERNAL_MODEL: &str = "gpt-5.4-mini";
|
||||
pub const CODEX_OPENAI_IMAGE_DEFAULT_MODEL: &str = "gpt-image-2";
|
||||
pub const CODEX_OPENAI_IMAGE_DEFAULT_VARIATION_MODEL: &str = "dall-e-2";
|
||||
pub const CODEX_OPENAI_IMAGE_DEFAULT_OUTPUT_FORMAT: &str = "png";
|
||||
pub const CODEX_OPENAI_IMAGE_DEFAULT_VARIATION_PROMPT: &str =
|
||||
"Create a faithful variation of the provided image.";
|
||||
const CODEX_IMAGE_TOOL_DEFAULT_SIZE: &str = "1024x1024";
|
||||
const CODEX_IMAGE_TOOL_DEFAULT_QUALITY: &str = "high";
|
||||
const CODEX_IMAGE_TOOL_DEFAULT_BACKGROUND: &str = "auto";
|
||||
const UUID_NAMESPACE_OID_BYTES: [u8; 16] = [
|
||||
0x6b, 0xa7, 0xb8, 0x12, 0x9d, 0xad, 0x11, 0xd1, 0x80, 0xb4, 0x00, 0xc0, 0x4f, 0xd4, 0x30, 0xc8,
|
||||
];
|
||||
|
||||
fn is_codex_openai_responses_request(provider_type: &str, provider_api_format: &str) -> bool {
|
||||
provider_type.trim().eq_ignore_ascii_case("codex")
|
||||
&& (aether_ai_formats::is_openai_responses_family_format(provider_api_format)
|
||||
|| is_openai_image_request(provider_api_format))
|
||||
}
|
||||
|
||||
fn is_openai_responses_compact_request(provider_api_format: &str) -> bool {
|
||||
aether_ai_formats::is_openai_responses_compact_format(provider_api_format)
|
||||
}
|
||||
|
||||
fn is_openai_image_request(provider_api_format: &str) -> bool {
|
||||
provider_api_format
|
||||
.trim()
|
||||
.eq_ignore_ascii_case("openai:image")
|
||||
}
|
||||
|
||||
fn apply_codex_openai_image_tool_overrides(body_object: &mut serde_json::Map<String, Value>) {
|
||||
let mut tool = body_object
|
||||
.get("tools")
|
||||
.and_then(Value::as_array)
|
||||
.and_then(|tools| tools.first())
|
||||
.and_then(Value::as_object)
|
||||
.cloned()
|
||||
.unwrap_or_default();
|
||||
|
||||
tool.insert("type".to_string(), json!("image_generation"));
|
||||
tool.entry("output_format".to_string())
|
||||
.or_insert_with(|| json!(CODEX_OPENAI_IMAGE_DEFAULT_OUTPUT_FORMAT));
|
||||
let action = tool
|
||||
.get("action")
|
||||
.and_then(Value::as_str)
|
||||
.map(str::trim)
|
||||
.filter(|value| !value.is_empty())
|
||||
.unwrap_or("generate")
|
||||
.to_string();
|
||||
if !tool.contains_key("action") {
|
||||
tool.insert("action".to_string(), json!("generate"));
|
||||
}
|
||||
if action == "generate" {
|
||||
tool.entry("size".to_string())
|
||||
.or_insert_with(|| json!(CODEX_IMAGE_TOOL_DEFAULT_SIZE));
|
||||
tool.entry("quality".to_string())
|
||||
.or_insert_with(|| json!(CODEX_IMAGE_TOOL_DEFAULT_QUALITY));
|
||||
tool.entry("background".to_string())
|
||||
.or_insert_with(|| json!(CODEX_IMAGE_TOOL_DEFAULT_BACKGROUND));
|
||||
}
|
||||
|
||||
body_object.insert("tools".to_string(), json!([tool]));
|
||||
body_object.insert(
|
||||
"tool_choice".to_string(),
|
||||
json!({
|
||||
"type": "image_generation"
|
||||
}),
|
||||
);
|
||||
}
|
||||
|
||||
fn codex_openai_image_has_prompt(body_object: &serde_json::Map<String, Value>) -> bool {
|
||||
body_object
|
||||
.get("input")
|
||||
.and_then(Value::as_array)
|
||||
.into_iter()
|
||||
.flatten()
|
||||
.filter_map(Value::as_object)
|
||||
.filter_map(|item| item.get("content"))
|
||||
.any(|content| match content {
|
||||
Value::String(text) => !text.trim().is_empty(),
|
||||
Value::Array(items) => items.iter().any(|item| {
|
||||
item.as_object()
|
||||
.filter(|item| item.get("type").and_then(Value::as_str) == Some("input_text"))
|
||||
.and_then(|item| item.get("text").and_then(Value::as_str))
|
||||
.map(str::trim)
|
||||
.is_some_and(|text| !text.is_empty())
|
||||
}),
|
||||
_ => false,
|
||||
})
|
||||
}
|
||||
|
||||
fn inject_codex_default_variation_prompt(body_object: &mut serde_json::Map<String, Value>) {
|
||||
let Some(action) = body_object
|
||||
.get("tools")
|
||||
.and_then(Value::as_array)
|
||||
.and_then(|tools| tools.first())
|
||||
.and_then(Value::as_object)
|
||||
.and_then(|tool| tool.get("action"))
|
||||
.and_then(Value::as_str)
|
||||
else {
|
||||
return;
|
||||
};
|
||||
if action != "edit" || codex_openai_image_has_prompt(body_object) {
|
||||
return;
|
||||
}
|
||||
|
||||
let Some(input) = body_object.get_mut("input").and_then(Value::as_array_mut) else {
|
||||
return;
|
||||
};
|
||||
let Some(first_message) = input.first_mut().and_then(Value::as_object_mut) else {
|
||||
return;
|
||||
};
|
||||
let Some(content) = first_message
|
||||
.get_mut("content")
|
||||
.and_then(Value::as_array_mut)
|
||||
else {
|
||||
return;
|
||||
};
|
||||
|
||||
content.insert(
|
||||
0,
|
||||
json!({
|
||||
"type": "input_text",
|
||||
"text": CODEX_OPENAI_IMAGE_DEFAULT_VARIATION_PROMPT,
|
||||
}),
|
||||
);
|
||||
}
|
||||
|
||||
fn build_stable_codex_prompt_cache_key(user_api_key_id: &str) -> Option<String> {
|
||||
let normalized = user_api_key_id.trim();
|
||||
if normalized.is_empty() {
|
||||
return None;
|
||||
}
|
||||
|
||||
let namespace = format!(
|
||||
"aether:codex:prompt-cache:{CODEX_PROMPT_CACHE_NAMESPACE_VERSION}:user:{normalized}"
|
||||
);
|
||||
let mut hasher = Sha1::new();
|
||||
hasher.update(UUID_NAMESPACE_OID_BYTES);
|
||||
hasher.update(namespace.as_bytes());
|
||||
|
||||
let digest = hasher.finalize();
|
||||
let mut bytes = [0u8; 16];
|
||||
bytes.copy_from_slice(&digest[..16]);
|
||||
bytes[6] = (bytes[6] & 0x0f) | 0x50;
|
||||
bytes[8] = (bytes[8] & 0x3f) | 0x80;
|
||||
Some(Uuid::from_bytes(bytes).to_string())
|
||||
}
|
||||
|
||||
fn build_short_codex_header_id(seed: &str) -> Option<String> {
|
||||
let normalized = seed.trim();
|
||||
if normalized.is_empty() {
|
||||
return None;
|
||||
}
|
||||
|
||||
let digest = Sha256::digest(normalized.as_bytes());
|
||||
let mut short_id = String::with_capacity(16);
|
||||
for byte in digest.iter().take(8) {
|
||||
let _ = write!(&mut short_id, "{byte:02x}");
|
||||
}
|
||||
Some(short_id)
|
||||
}
|
||||
|
||||
fn header_map_has_non_empty_value(headers: &http::HeaderMap, header_name: &str) -> bool {
|
||||
let target = header_name.trim().to_ascii_lowercase();
|
||||
if target.is_empty() {
|
||||
return false;
|
||||
}
|
||||
|
||||
headers.iter().any(|(name, value)| {
|
||||
if name.as_str().trim().to_ascii_lowercase() != target {
|
||||
return false;
|
||||
}
|
||||
value
|
||||
.to_str()
|
||||
.ok()
|
||||
.map(str::trim)
|
||||
.map(|value| !value.is_empty())
|
||||
.unwrap_or(false)
|
||||
})
|
||||
}
|
||||
|
||||
fn btree_map_has_non_empty_value(headers: &BTreeMap<String, String>, header_name: &str) -> bool {
|
||||
let target = header_name.trim().to_ascii_lowercase();
|
||||
if target.is_empty() {
|
||||
return false;
|
||||
}
|
||||
|
||||
headers
|
||||
.iter()
|
||||
.any(|(name, value)| name.trim().eq_ignore_ascii_case(&target) && !value.trim().is_empty())
|
||||
}
|
||||
|
||||
fn extract_codex_account_id(decrypted_auth_config_raw: Option<&str>) -> Option<String> {
|
||||
let raw = decrypted_auth_config_raw?.trim();
|
||||
if raw.is_empty() {
|
||||
return None;
|
||||
}
|
||||
|
||||
serde_json::from_str::<Value>(raw).ok().and_then(|value| {
|
||||
value
|
||||
.get("account_id")
|
||||
.and_then(Value::as_str)
|
||||
.map(str::trim)
|
||||
.filter(|value| !value.is_empty())
|
||||
.map(ToOwned::to_owned)
|
||||
})
|
||||
}
|
||||
|
||||
fn maybe_insert_default_codex_header(
|
||||
provider_request_headers: &mut BTreeMap<String, String>,
|
||||
original_headers: &http::HeaderMap,
|
||||
header_name: &str,
|
||||
header_value: &str,
|
||||
) {
|
||||
if header_map_has_non_empty_value(original_headers, header_name)
|
||||
|| btree_map_has_non_empty_value(provider_request_headers, header_name)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
provider_request_headers.insert(header_name.to_string(), header_value.to_string());
|
||||
}
|
||||
|
||||
fn maybe_inject_codex_prompt_cache_key(
|
||||
provider_request_body: &mut Value,
|
||||
provider_type: &str,
|
||||
provider_api_format: &str,
|
||||
user_api_key_id: Option<&str>,
|
||||
) {
|
||||
if !is_codex_openai_responses_request(provider_type, provider_api_format) {
|
||||
return;
|
||||
}
|
||||
|
||||
let Some(body_object) = provider_request_body.as_object_mut() else {
|
||||
return;
|
||||
};
|
||||
|
||||
let existing = body_object
|
||||
.get("prompt_cache_key")
|
||||
.and_then(Value::as_str)
|
||||
.map(str::trim)
|
||||
.unwrap_or_default();
|
||||
if !existing.is_empty() {
|
||||
return;
|
||||
}
|
||||
|
||||
let Some(prompt_cache_key) = user_api_key_id.and_then(build_stable_codex_prompt_cache_key)
|
||||
else {
|
||||
return;
|
||||
};
|
||||
|
||||
body_object.insert(
|
||||
"prompt_cache_key".to_string(),
|
||||
Value::String(prompt_cache_key),
|
||||
);
|
||||
}
|
||||
|
||||
pub fn apply_openai_responses_compact_special_body_edits(
|
||||
provider_request_body: &mut Value,
|
||||
provider_api_format: &str,
|
||||
) {
|
||||
if !is_openai_responses_compact_request(provider_api_format) {
|
||||
return;
|
||||
}
|
||||
|
||||
let Some(body_object) = provider_request_body.as_object_mut() else {
|
||||
return;
|
||||
};
|
||||
|
||||
// `/v1/responses/compact` does not accept `store`.
|
||||
body_object.remove("store");
|
||||
}
|
||||
|
||||
pub fn apply_codex_openai_responses_special_body_edits(
|
||||
provider_request_body: &mut Value,
|
||||
provider_type: &str,
|
||||
provider_api_format: &str,
|
||||
body_rules: Option<&Value>,
|
||||
user_api_key_id: Option<&str>,
|
||||
) {
|
||||
if !is_codex_openai_responses_request(provider_type, provider_api_format) {
|
||||
return;
|
||||
}
|
||||
|
||||
let Some(body_object) = provider_request_body.as_object_mut() else {
|
||||
return;
|
||||
};
|
||||
|
||||
if !body_rules_handle_path(body_rules, "max_output_tokens") {
|
||||
body_object.remove("max_output_tokens");
|
||||
}
|
||||
if !body_rules_handle_path(body_rules, "temperature") {
|
||||
body_object.remove("temperature");
|
||||
}
|
||||
if !body_rules_handle_path(body_rules, "top_p") {
|
||||
body_object.remove("top_p");
|
||||
}
|
||||
if !body_rules_handle_path(body_rules, "metadata") {
|
||||
body_object.remove("metadata");
|
||||
}
|
||||
if is_openai_responses_compact_request(provider_api_format) {
|
||||
body_object.remove("store");
|
||||
} else if !body_rules_handle_path(body_rules, "store") {
|
||||
body_object.insert("store".to_string(), json!(false));
|
||||
}
|
||||
if !body_rules_handle_path(body_rules, "instructions")
|
||||
&& !body_object.contains_key("instructions")
|
||||
{
|
||||
body_object.insert(
|
||||
"instructions".to_string(),
|
||||
json!(CODEX_DEFAULT_INSTRUCTIONS),
|
||||
);
|
||||
}
|
||||
if is_openai_image_request(provider_api_format) {
|
||||
body_object.insert(
|
||||
"model".to_string(),
|
||||
json!(CODEX_OPENAI_IMAGE_INTERNAL_MODEL),
|
||||
);
|
||||
body_object.insert("stream".to_string(), json!(true));
|
||||
apply_codex_openai_image_tool_overrides(body_object);
|
||||
inject_codex_default_variation_prompt(body_object);
|
||||
}
|
||||
|
||||
maybe_inject_codex_prompt_cache_key(
|
||||
provider_request_body,
|
||||
provider_type,
|
||||
provider_api_format,
|
||||
user_api_key_id,
|
||||
);
|
||||
}
|
||||
|
||||
pub fn apply_codex_openai_responses_special_headers(
|
||||
provider_request_headers: &mut BTreeMap<String, String>,
|
||||
provider_request_body: &Value,
|
||||
original_headers: &http::HeaderMap,
|
||||
provider_type: &str,
|
||||
provider_api_format: &str,
|
||||
request_id: Option<&str>,
|
||||
decrypted_auth_config_raw: Option<&str>,
|
||||
) {
|
||||
if !is_codex_openai_responses_request(provider_type, provider_api_format) {
|
||||
return;
|
||||
}
|
||||
|
||||
let prompt_cache_key = provider_request_body
|
||||
.get("prompt_cache_key")
|
||||
.and_then(Value::as_str)
|
||||
.map(str::trim)
|
||||
.filter(|value| !value.is_empty());
|
||||
|
||||
if !header_map_has_non_empty_value(original_headers, "chatgpt-account-id")
|
||||
&& !btree_map_has_non_empty_value(provider_request_headers, "chatgpt-account-id")
|
||||
{
|
||||
if let Some(account_id) = extract_codex_account_id(decrypted_auth_config_raw) {
|
||||
provider_request_headers.insert("chatgpt-account-id".to_string(), account_id);
|
||||
}
|
||||
}
|
||||
|
||||
if !header_map_has_non_empty_value(original_headers, "x-client-request-id")
|
||||
&& !btree_map_has_non_empty_value(provider_request_headers, "x-client-request-id")
|
||||
{
|
||||
if let Some(request_id) = request_id.map(str::trim).filter(|value| !value.is_empty()) {
|
||||
provider_request_headers
|
||||
.insert("x-client-request-id".to_string(), request_id.to_string());
|
||||
}
|
||||
}
|
||||
|
||||
if !is_openai_image_request(provider_api_format) {
|
||||
maybe_insert_default_codex_header(
|
||||
provider_request_headers,
|
||||
original_headers,
|
||||
"user-agent",
|
||||
CODEX_DEFAULT_USER_AGENT,
|
||||
);
|
||||
maybe_insert_default_codex_header(
|
||||
provider_request_headers,
|
||||
original_headers,
|
||||
"originator",
|
||||
CODEX_DEFAULT_ORIGINATOR,
|
||||
);
|
||||
}
|
||||
|
||||
let short_session_id = prompt_cache_key.and_then(build_short_codex_header_id);
|
||||
|
||||
if !header_map_has_non_empty_value(original_headers, "session_id")
|
||||
&& !btree_map_has_non_empty_value(provider_request_headers, "session_id")
|
||||
{
|
||||
if let Some(short_session_id) = short_session_id.as_deref() {
|
||||
provider_request_headers.insert("session_id".to_string(), short_session_id.to_string());
|
||||
}
|
||||
}
|
||||
|
||||
if aether_ai_formats::is_openai_responses_format(provider_api_format)
|
||||
&& !header_map_has_non_empty_value(original_headers, "conversation_id")
|
||||
&& !btree_map_has_non_empty_value(provider_request_headers, "conversation_id")
|
||||
{
|
||||
if let Some(short_session_id) = short_session_id.as_deref() {
|
||||
provider_request_headers
|
||||
.insert("conversation_id".to_string(), short_session_id.to_string());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::{
|
||||
apply_codex_openai_responses_special_body_edits, CODEX_OPENAI_IMAGE_INTERNAL_MODEL,
|
||||
};
|
||||
use serde_json::json;
|
||||
|
||||
#[test]
|
||||
fn codex_image_body_edits_force_tool_choice_and_default_generate_tool_fields() {
|
||||
let mut provider_request_body = json!({
|
||||
"input": [{
|
||||
"role": "user",
|
||||
"content": "generate image"
|
||||
}],
|
||||
"tools": [{
|
||||
"type": "image_generation"
|
||||
}],
|
||||
"tool_choice": "auto"
|
||||
});
|
||||
|
||||
apply_codex_openai_responses_special_body_edits(
|
||||
&mut provider_request_body,
|
||||
"codex",
|
||||
"openai:image",
|
||||
None,
|
||||
None,
|
||||
);
|
||||
|
||||
assert_eq!(
|
||||
provider_request_body["tools"][0]["size"],
|
||||
json!("1024x1024")
|
||||
);
|
||||
assert_eq!(provider_request_body["tools"][0]["quality"], json!("high"));
|
||||
assert_eq!(
|
||||
provider_request_body["tools"][0]["background"],
|
||||
json!("auto")
|
||||
);
|
||||
assert_eq!(
|
||||
provider_request_body["tools"][0]["output_format"],
|
||||
json!("png")
|
||||
);
|
||||
assert_eq!(
|
||||
provider_request_body["tools"][0]["action"],
|
||||
json!("generate")
|
||||
);
|
||||
assert_eq!(
|
||||
provider_request_body["model"],
|
||||
json!(CODEX_OPENAI_IMAGE_INTERNAL_MODEL)
|
||||
);
|
||||
assert_eq!(provider_request_body["stream"], json!(true));
|
||||
assert_eq!(
|
||||
provider_request_body["tool_choice"]["type"],
|
||||
json!("image_generation")
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn codex_image_body_edits_preserve_edit_action_without_generate_defaults() {
|
||||
let mut provider_request_body = json!({
|
||||
"tools": [{
|
||||
"type": "image_generation",
|
||||
"action": "edit",
|
||||
"input_image_mask": { "image_url": "data:image/png;base64,mask" }
|
||||
}],
|
||||
"input": [{
|
||||
"role": "user",
|
||||
"content": [{
|
||||
"type": "input_image",
|
||||
"image_url": "data:image/png;base64,image"
|
||||
}]
|
||||
}],
|
||||
"tool_choice": "auto"
|
||||
});
|
||||
|
||||
apply_codex_openai_responses_special_body_edits(
|
||||
&mut provider_request_body,
|
||||
"codex",
|
||||
"openai:image",
|
||||
None,
|
||||
None,
|
||||
);
|
||||
|
||||
assert_eq!(provider_request_body["tools"][0]["action"], json!("edit"));
|
||||
assert!(provider_request_body["tools"][0].get("size").is_none());
|
||||
assert!(provider_request_body["tools"][0].get("quality").is_none());
|
||||
assert!(provider_request_body["tools"][0]
|
||||
.get("background")
|
||||
.is_none());
|
||||
assert_eq!(
|
||||
provider_request_body["tools"][0]["output_format"],
|
||||
json!("png")
|
||||
);
|
||||
assert_eq!(
|
||||
provider_request_body["input"][0]["content"][0]["text"],
|
||||
json!("Create a faithful variation of the provided image.")
|
||||
);
|
||||
assert_eq!(
|
||||
provider_request_body["tool_choice"]["type"],
|
||||
json!("image_generation")
|
||||
);
|
||||
}
|
||||
}
|
||||
21
crates/aether-ai-formats/src/request/standard/family.rs
Normal file
21
crates/aether-ai-formats/src/request/standard/family.rs
Normal file
@@ -0,0 +1,21 @@
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
||||
pub enum LocalStandardSourceFamily {
|
||||
Standard,
|
||||
Gemini,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
||||
pub enum LocalStandardSourceMode {
|
||||
Chat,
|
||||
Cli,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Copy)]
|
||||
pub struct LocalStandardSpec {
|
||||
pub api_format: &'static str,
|
||||
pub decision_kind: &'static str,
|
||||
pub report_kind: &'static str,
|
||||
pub family: LocalStandardSourceFamily,
|
||||
pub mode: LocalStandardSourceMode,
|
||||
pub require_streaming: bool,
|
||||
}
|
||||
53
crates/aether-ai-formats/src/request/standard/gemini/chat.rs
Normal file
53
crates/aether-ai-formats/src/request/standard/gemini/chat.rs
Normal file
@@ -0,0 +1,53 @@
|
||||
use crate::contracts::{GEMINI_CHAT_STREAM_PLAN_KIND, GEMINI_CHAT_SYNC_PLAN_KIND};
|
||||
use crate::request::standard::family::{
|
||||
LocalStandardSourceFamily, LocalStandardSourceMode, LocalStandardSpec,
|
||||
};
|
||||
|
||||
pub fn resolve_sync_spec(plan_kind: &str) -> Option<LocalStandardSpec> {
|
||||
match plan_kind {
|
||||
GEMINI_CHAT_SYNC_PLAN_KIND => Some(LocalStandardSpec {
|
||||
api_format: "gemini:generate_content",
|
||||
decision_kind: GEMINI_CHAT_SYNC_PLAN_KIND,
|
||||
report_kind: "gemini_chat_sync_finalize",
|
||||
family: LocalStandardSourceFamily::Gemini,
|
||||
mode: LocalStandardSourceMode::Chat,
|
||||
require_streaming: false,
|
||||
}),
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn resolve_stream_spec(plan_kind: &str) -> Option<LocalStandardSpec> {
|
||||
match plan_kind {
|
||||
GEMINI_CHAT_STREAM_PLAN_KIND => Some(LocalStandardSpec {
|
||||
api_format: "gemini:generate_content",
|
||||
decision_kind: GEMINI_CHAT_STREAM_PLAN_KIND,
|
||||
report_kind: "gemini_chat_stream_success",
|
||||
family: LocalStandardSourceFamily::Gemini,
|
||||
mode: LocalStandardSourceMode::Chat,
|
||||
require_streaming: true,
|
||||
}),
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::{resolve_stream_spec, resolve_sync_spec};
|
||||
|
||||
#[test]
|
||||
fn resolves_gemini_chat_sync_spec() {
|
||||
let spec = resolve_sync_spec("gemini_chat_sync").expect("spec");
|
||||
assert_eq!(spec.api_format, "gemini:generate_content");
|
||||
assert_eq!(spec.report_kind, "gemini_chat_sync_finalize");
|
||||
assert!(!spec.require_streaming);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn resolves_gemini_chat_stream_spec() {
|
||||
let spec = resolve_stream_spec("gemini_chat_stream").expect("spec");
|
||||
assert_eq!(spec.api_format, "gemini:generate_content");
|
||||
assert_eq!(spec.report_kind, "gemini_chat_stream_success");
|
||||
assert!(spec.require_streaming);
|
||||
}
|
||||
}
|
||||
53
crates/aether-ai-formats/src/request/standard/gemini/cli.rs
Normal file
53
crates/aether-ai-formats/src/request/standard/gemini/cli.rs
Normal file
@@ -0,0 +1,53 @@
|
||||
use crate::contracts::{GEMINI_CLI_STREAM_PLAN_KIND, GEMINI_CLI_SYNC_PLAN_KIND};
|
||||
use crate::request::standard::family::{
|
||||
LocalStandardSourceFamily, LocalStandardSourceMode, LocalStandardSpec,
|
||||
};
|
||||
|
||||
pub fn resolve_sync_spec(plan_kind: &str) -> Option<LocalStandardSpec> {
|
||||
match plan_kind {
|
||||
GEMINI_CLI_SYNC_PLAN_KIND => Some(LocalStandardSpec {
|
||||
api_format: "gemini:generate_content",
|
||||
decision_kind: GEMINI_CLI_SYNC_PLAN_KIND,
|
||||
report_kind: "gemini_cli_sync_finalize",
|
||||
family: LocalStandardSourceFamily::Gemini,
|
||||
mode: LocalStandardSourceMode::Cli,
|
||||
require_streaming: false,
|
||||
}),
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn resolve_stream_spec(plan_kind: &str) -> Option<LocalStandardSpec> {
|
||||
match plan_kind {
|
||||
GEMINI_CLI_STREAM_PLAN_KIND => Some(LocalStandardSpec {
|
||||
api_format: "gemini:generate_content",
|
||||
decision_kind: GEMINI_CLI_STREAM_PLAN_KIND,
|
||||
report_kind: "gemini_cli_stream_success",
|
||||
family: LocalStandardSourceFamily::Gemini,
|
||||
mode: LocalStandardSourceMode::Cli,
|
||||
require_streaming: true,
|
||||
}),
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::{resolve_stream_spec, resolve_sync_spec};
|
||||
|
||||
#[test]
|
||||
fn resolves_gemini_cli_sync_spec() {
|
||||
let spec = resolve_sync_spec("gemini_cli_sync").expect("spec");
|
||||
assert_eq!(spec.api_format, "gemini:generate_content");
|
||||
assert_eq!(spec.report_kind, "gemini_cli_sync_finalize");
|
||||
assert!(!spec.require_streaming);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn resolves_gemini_cli_stream_spec() {
|
||||
let spec = resolve_stream_spec("gemini_cli_stream").expect("spec");
|
||||
assert_eq!(spec.api_format, "gemini:generate_content");
|
||||
assert_eq!(spec.report_kind, "gemini_cli_stream_success");
|
||||
assert!(spec.require_streaming);
|
||||
}
|
||||
}
|
||||
12
crates/aether-ai-formats/src/request/standard/gemini/mod.rs
Normal file
12
crates/aether-ai-formats/src/request/standard/gemini/mod.rs
Normal file
@@ -0,0 +1,12 @@
|
||||
pub mod chat;
|
||||
pub mod cli;
|
||||
|
||||
use crate::request::standard::LocalStandardSpec;
|
||||
|
||||
pub fn resolve_sync_spec(plan_kind: &str) -> Option<LocalStandardSpec> {
|
||||
chat::resolve_sync_spec(plan_kind).or_else(|| cli::resolve_sync_spec(plan_kind))
|
||||
}
|
||||
|
||||
pub fn resolve_stream_spec(plan_kind: &str) -> Option<LocalStandardSpec> {
|
||||
chat::resolve_stream_spec(plan_kind).or_else(|| cli::resolve_stream_spec(plan_kind))
|
||||
}
|
||||
1095
crates/aether-ai-formats/src/request/standard/matrix.rs
Normal file
1095
crates/aether-ai-formats/src/request/standard/matrix.rs
Normal file
File diff suppressed because it is too large
Load Diff
20
crates/aether-ai-formats/src/request/standard/mod.rs
Normal file
20
crates/aether-ai-formats/src/request/standard/mod.rs
Normal file
@@ -0,0 +1,20 @@
|
||||
pub mod claude;
|
||||
pub mod codex;
|
||||
pub mod family;
|
||||
pub mod gemini;
|
||||
pub mod matrix;
|
||||
pub mod normalize;
|
||||
pub mod openai_responses;
|
||||
|
||||
pub use 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,
|
||||
};
|
||||
pub use family::{LocalStandardSourceFamily, LocalStandardSourceMode, LocalStandardSpec};
|
||||
pub use matrix::{build_standard_request_body, normalize_standard_request_to_openai_chat_request};
|
||||
pub use normalize::{
|
||||
build_cross_format_openai_chat_request_body, build_cross_format_openai_responses_request_body,
|
||||
build_local_openai_chat_request_body, build_local_openai_responses_request_body,
|
||||
};
|
||||
232
crates/aether-ai-formats/src/request/standard/normalize.rs
Normal file
232
crates/aether-ai-formats/src/request/standard/normalize.rs
Normal file
@@ -0,0 +1,232 @@
|
||||
use aether_ai_formats::protocol::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,
|
||||
};
|
||||
use aether_ai_formats::{request_conversion_kind, RequestConversionKind};
|
||||
use serde_json::{json, Value};
|
||||
|
||||
pub fn build_local_openai_chat_request_body(
|
||||
body_json: &Value,
|
||||
mapped_model: &str,
|
||||
upstream_is_stream: bool,
|
||||
) -> Option<Value> {
|
||||
let request_body_object = body_json.as_object()?;
|
||||
let mut provider_request_body = serde_json::Map::from_iter(
|
||||
request_body_object
|
||||
.iter()
|
||||
.map(|(key, value)| (key.clone(), value.clone())),
|
||||
);
|
||||
provider_request_body.insert("model".to_string(), Value::String(mapped_model.to_string()));
|
||||
if upstream_is_stream {
|
||||
provider_request_body.insert("stream".to_string(), Value::Bool(true));
|
||||
match provider_request_body.get_mut("stream_options") {
|
||||
Some(Value::Object(stream_options)) => {
|
||||
stream_options.insert("include_usage".to_string(), Value::Bool(true));
|
||||
}
|
||||
_ => {
|
||||
provider_request_body.insert(
|
||||
"stream_options".to_string(),
|
||||
json!({
|
||||
"include_usage": true,
|
||||
}),
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
Some(Value::Object(provider_request_body))
|
||||
}
|
||||
|
||||
pub fn build_cross_format_openai_chat_request_body(
|
||||
body_json: &Value,
|
||||
mapped_model: &str,
|
||||
provider_api_format: &str,
|
||||
upstream_is_stream: bool,
|
||||
) -> Option<Value> {
|
||||
let conversion_kind = request_conversion_kind("openai:chat", provider_api_format)?;
|
||||
match conversion_kind {
|
||||
RequestConversionKind::ToClaudeStandard => convert_openai_chat_request_to_claude_request(
|
||||
body_json,
|
||||
mapped_model,
|
||||
upstream_is_stream,
|
||||
),
|
||||
RequestConversionKind::ToGeminiStandard => convert_openai_chat_request_to_gemini_request(
|
||||
body_json,
|
||||
mapped_model,
|
||||
upstream_is_stream,
|
||||
),
|
||||
RequestConversionKind::ToOpenAiResponses => {
|
||||
convert_openai_chat_request_to_openai_responses_request(
|
||||
body_json,
|
||||
mapped_model,
|
||||
upstream_is_stream,
|
||||
false,
|
||||
)
|
||||
}
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn build_local_openai_responses_request_body(
|
||||
body_json: &Value,
|
||||
mapped_model: &str,
|
||||
require_streaming: bool,
|
||||
) -> Option<Value> {
|
||||
let request_body_object = body_json.as_object()?;
|
||||
let mut provider_request_body = serde_json::Map::from_iter(
|
||||
request_body_object
|
||||
.iter()
|
||||
.map(|(key, value)| (key.clone(), value.clone())),
|
||||
);
|
||||
provider_request_body.insert("model".to_string(), Value::String(mapped_model.to_string()));
|
||||
if require_streaming {
|
||||
provider_request_body.insert("stream".to_string(), Value::Bool(true));
|
||||
}
|
||||
Some(Value::Object(provider_request_body))
|
||||
}
|
||||
|
||||
pub fn build_cross_format_openai_responses_request_body(
|
||||
body_json: &Value,
|
||||
mapped_model: &str,
|
||||
client_api_format: &str,
|
||||
provider_api_format: &str,
|
||||
upstream_is_stream: bool,
|
||||
) -> Option<Value> {
|
||||
let chat_like_request = normalize_openai_responses_request_to_openai_chat_request(body_json)?;
|
||||
let conversion_kind = request_conversion_kind(client_api_format, provider_api_format)?;
|
||||
match conversion_kind {
|
||||
RequestConversionKind::ToOpenAIChat => build_local_openai_chat_request_body(
|
||||
&chat_like_request,
|
||||
mapped_model,
|
||||
upstream_is_stream,
|
||||
),
|
||||
RequestConversionKind::ToOpenAiResponses => {
|
||||
convert_openai_chat_request_to_openai_responses_request(
|
||||
&chat_like_request,
|
||||
mapped_model,
|
||||
upstream_is_stream,
|
||||
false,
|
||||
)
|
||||
}
|
||||
RequestConversionKind::ToClaudeStandard => convert_openai_chat_request_to_claude_request(
|
||||
&chat_like_request,
|
||||
mapped_model,
|
||||
upstream_is_stream,
|
||||
),
|
||||
RequestConversionKind::ToGeminiStandard => convert_openai_chat_request_to_gemini_request(
|
||||
&chat_like_request,
|
||||
mapped_model,
|
||||
upstream_is_stream,
|
||||
),
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::build_local_openai_responses_request_body;
|
||||
use super::{
|
||||
build_cross_format_openai_responses_request_body, build_local_openai_chat_request_body,
|
||||
};
|
||||
use serde_json::{json, Value};
|
||||
|
||||
fn object_keys(value: &Value) -> Vec<&str> {
|
||||
value
|
||||
.as_object()
|
||||
.expect("json object")
|
||||
.keys()
|
||||
.map(String::as_str)
|
||||
.collect()
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn builds_openai_chat_cross_format_request_body_from_openai_responses_source() {
|
||||
let body_json = json!({
|
||||
"model": "gpt-5",
|
||||
"input": "hello",
|
||||
});
|
||||
|
||||
let provider_request_body = build_cross_format_openai_responses_request_body(
|
||||
&body_json,
|
||||
"gpt-5-upstream",
|
||||
"openai:responses",
|
||||
"openai:chat",
|
||||
false,
|
||||
)
|
||||
.expect("openai responses to openai chat body should build");
|
||||
|
||||
assert_eq!(provider_request_body["model"], "gpt-5-upstream");
|
||||
assert_eq!(provider_request_body["messages"][0]["role"], "user");
|
||||
assert_eq!(provider_request_body["messages"][0]["content"], "hello");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn local_openai_responses_request_body_preserves_original_field_order() {
|
||||
let body_json: Value = serde_json::from_str(
|
||||
r#"{
|
||||
"model": "gpt-5",
|
||||
"include": ["reasoning.encrypted_content"],
|
||||
"input": [],
|
||||
"instructions": "Keep order"
|
||||
}"#,
|
||||
)
|
||||
.expect("request json should parse");
|
||||
|
||||
let provider_request_body =
|
||||
build_local_openai_responses_request_body(&body_json, "gpt-5-upstream", false)
|
||||
.expect("openai responses body should build");
|
||||
|
||||
assert_eq!(
|
||||
object_keys(&provider_request_body),
|
||||
vec!["model", "include", "input", "instructions"]
|
||||
);
|
||||
assert_eq!(provider_request_body["model"], "gpt-5-upstream");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn builds_streaming_local_openai_chat_request_body_with_include_usage() {
|
||||
let body_json = json!({
|
||||
"model": "gpt-5",
|
||||
"messages": [{
|
||||
"role": "user",
|
||||
"content": "hello"
|
||||
}]
|
||||
});
|
||||
|
||||
let provider_request_body =
|
||||
build_local_openai_chat_request_body(&body_json, "gpt-5-upstream", true)
|
||||
.expect("openai chat body should build");
|
||||
|
||||
assert_eq!(provider_request_body["model"], "gpt-5-upstream");
|
||||
assert_eq!(provider_request_body["stream"], true);
|
||||
assert_eq!(
|
||||
provider_request_body["stream_options"]["include_usage"],
|
||||
true
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn streaming_local_openai_chat_request_body_preserves_stream_options_while_forcing_include_usage(
|
||||
) {
|
||||
let body_json = json!({
|
||||
"model": "gpt-5",
|
||||
"messages": [{
|
||||
"role": "user",
|
||||
"content": "hello"
|
||||
}],
|
||||
"stream_options": {
|
||||
"include_usage": false,
|
||||
"extra": "keep-me"
|
||||
}
|
||||
});
|
||||
|
||||
let provider_request_body =
|
||||
build_local_openai_chat_request_body(&body_json, "gpt-5-upstream", true)
|
||||
.expect("openai chat body should build");
|
||||
|
||||
assert_eq!(
|
||||
provider_request_body["stream_options"]["include_usage"],
|
||||
true
|
||||
);
|
||||
assert_eq!(provider_request_body["stream_options"]["extra"], "keep-me");
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,78 @@
|
||||
use crate::contracts::{
|
||||
OPENAI_RESPONSES_COMPACT_STREAM_PLAN_KIND, OPENAI_RESPONSES_COMPACT_STREAM_SUCCESS_REPORT_KIND,
|
||||
OPENAI_RESPONSES_COMPACT_SYNC_PLAN_KIND, OPENAI_RESPONSES_COMPACT_SYNC_SUCCESS_REPORT_KIND,
|
||||
OPENAI_RESPONSES_STREAM_PLAN_KIND, OPENAI_RESPONSES_STREAM_SUCCESS_REPORT_KIND,
|
||||
OPENAI_RESPONSES_SYNC_PLAN_KIND, OPENAI_RESPONSES_SYNC_SUCCESS_REPORT_KIND,
|
||||
};
|
||||
|
||||
#[derive(Debug, Clone, Copy)]
|
||||
pub struct LocalOpenAiResponsesSpec {
|
||||
pub api_format: &'static str,
|
||||
pub decision_kind: &'static str,
|
||||
pub report_kind: &'static str,
|
||||
pub compact: bool,
|
||||
pub require_streaming: bool,
|
||||
}
|
||||
|
||||
pub fn resolve_sync_spec(plan_kind: &str) -> Option<LocalOpenAiResponsesSpec> {
|
||||
match plan_kind {
|
||||
OPENAI_RESPONSES_SYNC_PLAN_KIND => Some(LocalOpenAiResponsesSpec {
|
||||
api_format: "openai:responses",
|
||||
decision_kind: OPENAI_RESPONSES_SYNC_PLAN_KIND,
|
||||
report_kind: OPENAI_RESPONSES_SYNC_SUCCESS_REPORT_KIND,
|
||||
compact: false,
|
||||
require_streaming: false,
|
||||
}),
|
||||
OPENAI_RESPONSES_COMPACT_SYNC_PLAN_KIND => Some(LocalOpenAiResponsesSpec {
|
||||
api_format: "openai:responses:compact",
|
||||
decision_kind: OPENAI_RESPONSES_COMPACT_SYNC_PLAN_KIND,
|
||||
report_kind: OPENAI_RESPONSES_COMPACT_SYNC_SUCCESS_REPORT_KIND,
|
||||
compact: true,
|
||||
require_streaming: false,
|
||||
}),
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn resolve_stream_spec(plan_kind: &str) -> Option<LocalOpenAiResponsesSpec> {
|
||||
match plan_kind {
|
||||
OPENAI_RESPONSES_STREAM_PLAN_KIND => Some(LocalOpenAiResponsesSpec {
|
||||
api_format: "openai:responses",
|
||||
decision_kind: OPENAI_RESPONSES_STREAM_PLAN_KIND,
|
||||
report_kind: OPENAI_RESPONSES_STREAM_SUCCESS_REPORT_KIND,
|
||||
compact: false,
|
||||
require_streaming: true,
|
||||
}),
|
||||
OPENAI_RESPONSES_COMPACT_STREAM_PLAN_KIND => Some(LocalOpenAiResponsesSpec {
|
||||
api_format: "openai:responses:compact",
|
||||
decision_kind: OPENAI_RESPONSES_COMPACT_STREAM_PLAN_KIND,
|
||||
report_kind: OPENAI_RESPONSES_COMPACT_STREAM_SUCCESS_REPORT_KIND,
|
||||
compact: true,
|
||||
require_streaming: true,
|
||||
}),
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::{resolve_stream_spec, resolve_sync_spec};
|
||||
|
||||
#[test]
|
||||
fn resolves_openai_responses_sync_spec() {
|
||||
let spec = resolve_sync_spec("openai_responses_sync").expect("spec");
|
||||
assert_eq!(spec.api_format, "openai:responses");
|
||||
assert_eq!(spec.report_kind, "openai_responses_sync_success");
|
||||
assert!(!spec.compact);
|
||||
assert!(!spec.require_streaming);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn resolves_openai_responses_compact_stream_spec() {
|
||||
let spec = resolve_stream_spec("openai_responses_compact_stream").expect("spec");
|
||||
assert_eq!(spec.api_format, "openai:responses:compact");
|
||||
assert_eq!(spec.report_kind, "openai_responses_compact_stream_success");
|
||||
assert!(spec.compact);
|
||||
assert!(spec.require_streaming);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user