refactor: fold ai surfaces into formats

This commit is contained in:
fawney19
2026-05-02 18:19:39 +08:00
parent bfd1ea72b3
commit 47ee8b9c13
114 changed files with 600 additions and 622 deletions

View 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);
}
}

View 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);
}
}

View 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))
}

View 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")
);
}
}

View 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,
}

View 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);
}
}

View 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);
}
}

View 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))
}

File diff suppressed because it is too large Load Diff

View 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,
};

View 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");
}
}

View File

@@ -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);
}
}