mirror of
https://github.com/fawney19/Aether.git
synced 2026-09-03 09:50:21 +08:00
migrate ai format conversion to responses adapters
This commit is contained in:
@@ -26,9 +26,10 @@ pub fn force_upstream_streaming_for_provider(
|
||||
provider_api_format: &str,
|
||||
) -> bool {
|
||||
provider_type.trim().eq_ignore_ascii_case("codex")
|
||||
&& provider_api_format
|
||||
.trim()
|
||||
.eq_ignore_ascii_case("openai:cli")
|
||||
&& matches!(
|
||||
provider_api_format.trim().to_ascii_lowercase().as_str(),
|
||||
"openai:responses" | "openai:cli"
|
||||
)
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
@@ -57,7 +58,11 @@ mod tests {
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn forces_streaming_for_codex_openai_cli() {
|
||||
fn forces_streaming_for_codex_openai_responses() {
|
||||
assert!(force_upstream_streaming_for_provider(
|
||||
"codex",
|
||||
"openai:responses"
|
||||
));
|
||||
assert!(force_upstream_streaming_for_provider("codex", "openai:cli"));
|
||||
}
|
||||
|
||||
|
||||
@@ -9,6 +9,8 @@ use crate::contracts::{
|
||||
GEMINI_VIDEO_CREATE_SYNC_PLAN_KIND, OPENAI_CHAT_STREAM_PLAN_KIND, OPENAI_CHAT_SYNC_PLAN_KIND,
|
||||
OPENAI_CLI_STREAM_PLAN_KIND, OPENAI_CLI_SYNC_PLAN_KIND, OPENAI_COMPACT_STREAM_PLAN_KIND,
|
||||
OPENAI_COMPACT_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,
|
||||
@@ -74,19 +76,19 @@ pub fn resolve_execution_runtime_stream_plan_kind(
|
||||
}
|
||||
|
||||
if route_family == Some("openai")
|
||||
&& route_kind == Some("cli")
|
||||
&& is_openai_responses_route_kind(route_kind)
|
||||
&& *method == Method::POST
|
||||
&& path == "/v1/responses"
|
||||
{
|
||||
return Some(OPENAI_CLI_STREAM_PLAN_KIND);
|
||||
return Some(OPENAI_RESPONSES_STREAM_PLAN_KIND);
|
||||
}
|
||||
|
||||
if route_family == Some("openai")
|
||||
&& route_kind == Some("compact")
|
||||
&& is_openai_responses_compact_route_kind(route_kind)
|
||||
&& *method == Method::POST
|
||||
&& path == "/v1/responses/compact"
|
||||
{
|
||||
return Some(OPENAI_COMPACT_STREAM_PLAN_KIND);
|
||||
return Some(OPENAI_RESPONSES_COMPACT_STREAM_PLAN_KIND);
|
||||
}
|
||||
|
||||
if route_family == Some("openai")
|
||||
@@ -189,19 +191,19 @@ pub fn resolve_execution_runtime_sync_plan_kind(
|
||||
}
|
||||
|
||||
if route_family == Some("openai")
|
||||
&& route_kind == Some("cli")
|
||||
&& is_openai_responses_route_kind(route_kind)
|
||||
&& *method == Method::POST
|
||||
&& path == "/v1/responses"
|
||||
{
|
||||
return Some(OPENAI_CLI_SYNC_PLAN_KIND);
|
||||
return Some(OPENAI_RESPONSES_SYNC_PLAN_KIND);
|
||||
}
|
||||
|
||||
if route_family == Some("openai")
|
||||
&& route_kind == Some("compact")
|
||||
&& is_openai_responses_compact_route_kind(route_kind)
|
||||
&& *method == Method::POST
|
||||
&& path == "/v1/responses/compact"
|
||||
{
|
||||
return Some(OPENAI_COMPACT_SYNC_PLAN_KIND);
|
||||
return Some(OPENAI_RESPONSES_COMPACT_SYNC_PLAN_KIND);
|
||||
}
|
||||
|
||||
if route_family == Some("claude")
|
||||
@@ -260,6 +262,14 @@ pub fn resolve_execution_runtime_sync_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"))
|
||||
}
|
||||
|
||||
pub fn is_matching_stream_request(
|
||||
plan_kind: &str,
|
||||
path: &str,
|
||||
@@ -268,6 +278,8 @@ pub fn is_matching_stream_request(
|
||||
match plan_kind {
|
||||
OPENAI_CHAT_STREAM_PLAN_KIND
|
||||
| CLAUDE_CHAT_STREAM_PLAN_KIND
|
||||
| OPENAI_RESPONSES_STREAM_PLAN_KIND
|
||||
| OPENAI_RESPONSES_COMPACT_STREAM_PLAN_KIND
|
||||
| OPENAI_CLI_STREAM_PLAN_KIND
|
||||
| OPENAI_COMPACT_STREAM_PLAN_KIND
|
||||
| CLAUDE_CLI_STREAM_PLAN_KIND
|
||||
@@ -287,6 +299,8 @@ pub fn supports_sync_scheduler_decision_kind(plan_kind: &str) -> bool {
|
||||
plan_kind,
|
||||
OPENAI_CHAT_SYNC_PLAN_KIND
|
||||
| OPENAI_IMAGE_SYNC_PLAN_KIND
|
||||
| OPENAI_RESPONSES_SYNC_PLAN_KIND
|
||||
| OPENAI_RESPONSES_COMPACT_SYNC_PLAN_KIND
|
||||
| OPENAI_CLI_SYNC_PLAN_KIND
|
||||
| OPENAI_COMPACT_SYNC_PLAN_KIND
|
||||
| CLAUDE_CHAT_SYNC_PLAN_KIND
|
||||
@@ -312,6 +326,8 @@ pub fn supports_stream_scheduler_decision_kind(plan_kind: &str) -> bool {
|
||||
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_CLI_STREAM_PLAN_KIND
|
||||
| OPENAI_IMAGE_STREAM_PLAN_KIND
|
||||
| OPENAI_COMPACT_STREAM_PLAN_KIND
|
||||
@@ -333,7 +349,9 @@ mod tests {
|
||||
};
|
||||
use crate::contracts::{
|
||||
OPENAI_CHAT_STREAM_PLAN_KIND, OPENAI_CHAT_SYNC_PLAN_KIND, OPENAI_IMAGE_STREAM_PLAN_KIND,
|
||||
OPENAI_IMAGE_SYNC_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]
|
||||
@@ -360,6 +378,86 @@ mod tests {
|
||||
);
|
||||
}
|
||||
|
||||
#[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_scheduler_decision_kind(
|
||||
OPENAI_RESPONSES_SYNC_PLAN_KIND
|
||||
));
|
||||
assert!(supports_stream_scheduler_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_scheduler_decision_kind(
|
||||
OPENAI_RESPONSES_COMPACT_SYNC_PLAN_KIND
|
||||
));
|
||||
assert!(supports_stream_scheduler_decision_kind(
|
||||
OPENAI_RESPONSES_COMPACT_STREAM_PLAN_KIND
|
||||
));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn stream_matching_requires_openai_stream_flag() {
|
||||
assert!(!is_matching_stream_request(
|
||||
|
||||
@@ -25,18 +25,23 @@ 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_cli_request(provider_type: &str, provider_api_format: &str) -> bool {
|
||||
fn is_codex_openai_responses_request(provider_type: &str, provider_api_format: &str) -> bool {
|
||||
provider_type.trim().eq_ignore_ascii_case("codex")
|
||||
&& matches!(
|
||||
provider_api_format.trim().to_ascii_lowercase().as_str(),
|
||||
"openai:cli" | "openai:compact" | "openai:image"
|
||||
"openai:responses"
|
||||
| "openai:responses:compact"
|
||||
| "openai:cli"
|
||||
| "openai:compact"
|
||||
| "openai:image"
|
||||
)
|
||||
}
|
||||
|
||||
fn is_openai_compact_request(provider_api_format: &str) -> bool {
|
||||
provider_api_format
|
||||
.trim()
|
||||
.eq_ignore_ascii_case("openai:compact")
|
||||
matches!(
|
||||
provider_api_format.trim().to_ascii_lowercase().as_str(),
|
||||
"openai:responses:compact" | "openai:compact"
|
||||
)
|
||||
}
|
||||
|
||||
fn is_openai_image_request(provider_api_format: &str) -> bool {
|
||||
@@ -245,7 +250,7 @@ fn maybe_inject_codex_prompt_cache_key(
|
||||
provider_api_format: &str,
|
||||
user_api_key_id: Option<&str>,
|
||||
) {
|
||||
if !is_codex_openai_cli_request(provider_type, provider_api_format) {
|
||||
if !is_codex_openai_responses_request(provider_type, provider_api_format) {
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -273,7 +278,7 @@ fn maybe_inject_codex_prompt_cache_key(
|
||||
);
|
||||
}
|
||||
|
||||
pub fn apply_openai_compact_special_body_edits(
|
||||
pub fn apply_openai_responses_compact_special_body_edits(
|
||||
provider_request_body: &mut Value,
|
||||
provider_api_format: &str,
|
||||
) {
|
||||
@@ -289,14 +294,25 @@ pub fn apply_openai_compact_special_body_edits(
|
||||
body_object.remove("store");
|
||||
}
|
||||
|
||||
pub fn apply_codex_openai_cli_special_body_edits(
|
||||
#[deprecated(
|
||||
since = "0.1.0",
|
||||
note = "use apply_openai_responses_compact_special_body_edits"
|
||||
)]
|
||||
pub fn apply_openai_compact_special_body_edits(
|
||||
provider_request_body: &mut Value,
|
||||
provider_api_format: &str,
|
||||
) {
|
||||
apply_openai_responses_compact_special_body_edits(provider_request_body, provider_api_format);
|
||||
}
|
||||
|
||||
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_cli_request(provider_type, provider_api_format) {
|
||||
if !is_codex_openai_responses_request(provider_type, provider_api_format) {
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -347,7 +363,27 @@ pub fn apply_codex_openai_cli_special_body_edits(
|
||||
);
|
||||
}
|
||||
|
||||
pub fn apply_codex_openai_cli_special_headers(
|
||||
#[deprecated(
|
||||
since = "0.1.0",
|
||||
note = "use apply_codex_openai_responses_special_body_edits"
|
||||
)]
|
||||
pub fn apply_codex_openai_cli_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>,
|
||||
) {
|
||||
apply_codex_openai_responses_special_body_edits(
|
||||
provider_request_body,
|
||||
provider_type,
|
||||
provider_api_format,
|
||||
body_rules,
|
||||
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,
|
||||
@@ -356,7 +392,7 @@ pub fn apply_codex_openai_cli_special_headers(
|
||||
request_id: Option<&str>,
|
||||
decrypted_auth_config_raw: Option<&str>,
|
||||
) {
|
||||
if !is_codex_openai_cli_request(provider_type, provider_api_format) {
|
||||
if !is_codex_openai_responses_request(provider_type, provider_api_format) {
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -408,10 +444,10 @@ pub fn apply_codex_openai_cli_special_headers(
|
||||
}
|
||||
}
|
||||
|
||||
if provider_api_format
|
||||
.trim()
|
||||
.eq_ignore_ascii_case("openai:cli")
|
||||
&& !header_map_has_non_empty_value(original_headers, "conversation_id")
|
||||
if matches!(
|
||||
provider_api_format.trim().to_ascii_lowercase().as_str(),
|
||||
"openai:responses" | "openai:cli"
|
||||
) && !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() {
|
||||
@@ -421,9 +457,35 @@ pub fn apply_codex_openai_cli_special_headers(
|
||||
}
|
||||
}
|
||||
|
||||
#[deprecated(
|
||||
since = "0.1.0",
|
||||
note = "use apply_codex_openai_responses_special_headers"
|
||||
)]
|
||||
pub fn apply_codex_openai_cli_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>,
|
||||
) {
|
||||
apply_codex_openai_responses_special_headers(
|
||||
provider_request_headers,
|
||||
provider_request_body,
|
||||
original_headers,
|
||||
provider_type,
|
||||
provider_api_format,
|
||||
request_id,
|
||||
decrypted_auth_config_raw,
|
||||
);
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::{apply_codex_openai_cli_special_body_edits, CODEX_OPENAI_IMAGE_INTERNAL_MODEL};
|
||||
use super::{
|
||||
apply_codex_openai_responses_special_body_edits, CODEX_OPENAI_IMAGE_INTERNAL_MODEL,
|
||||
};
|
||||
use serde_json::json;
|
||||
|
||||
#[test]
|
||||
@@ -439,7 +501,7 @@ mod tests {
|
||||
"tool_choice": "auto"
|
||||
});
|
||||
|
||||
apply_codex_openai_cli_special_body_edits(
|
||||
apply_codex_openai_responses_special_body_edits(
|
||||
&mut provider_request_body,
|
||||
"codex",
|
||||
"openai:image",
|
||||
@@ -493,7 +555,7 @@ mod tests {
|
||||
"tool_choice": "auto"
|
||||
});
|
||||
|
||||
apply_codex_openai_cli_special_body_edits(
|
||||
apply_codex_openai_responses_special_body_edits(
|
||||
&mut provider_request_body,
|
||||
"codex",
|
||||
"openai:image",
|
||||
|
||||
@@ -1,22 +1,23 @@
|
||||
use std::borrow::Cow;
|
||||
|
||||
use aether_ai_formats::registry::{convert_request, FormatContext};
|
||||
use aether_provider_transport::{
|
||||
apply_local_body_rules, build_transport_request_url, GatewayProviderTransportSnapshot,
|
||||
TransportRequestUrlParams,
|
||||
};
|
||||
use serde_json::Value;
|
||||
|
||||
use super::{
|
||||
apply_openai_responses_compact_special_body_edits,
|
||||
codex::apply_codex_openai_responses_special_body_edits,
|
||||
normalize::build_local_openai_chat_request_body,
|
||||
};
|
||||
use crate::conversion::request::{
|
||||
convert_openai_chat_request_to_claude_request, convert_openai_chat_request_to_gemini_request,
|
||||
convert_openai_chat_request_to_openai_cli_request,
|
||||
convert_openai_chat_request_to_openai_responses_request,
|
||||
normalize_claude_request_to_openai_chat_request,
|
||||
normalize_gemini_request_to_openai_chat_request,
|
||||
normalize_openai_cli_request_to_openai_chat_request,
|
||||
};
|
||||
|
||||
use super::{
|
||||
apply_openai_compact_special_body_edits, codex::apply_codex_openai_cli_special_body_edits,
|
||||
normalize::build_local_openai_chat_request_body,
|
||||
normalize_openai_responses_request_to_openai_chat_request,
|
||||
};
|
||||
|
||||
#[allow(clippy::too_many_arguments)]
|
||||
@@ -31,29 +32,32 @@ pub fn build_standard_request_body(
|
||||
body_rules: Option<&Value>,
|
||||
user_api_key_id: Option<&str>,
|
||||
) -> Option<Value> {
|
||||
let canonical_request = normalize_standard_request_to_openai_chat_request_cow(
|
||||
body_json,
|
||||
let format_context = FormatContext::default()
|
||||
.with_mapped_model(mapped_model)
|
||||
.with_request_path(request_path)
|
||||
.with_upstream_stream(upstream_is_stream);
|
||||
let mut provider_request_body = convert_request(
|
||||
client_api_format,
|
||||
request_path,
|
||||
)?;
|
||||
let mut provider_request_body = build_standard_request_body_from_canonical(
|
||||
canonical_request.as_ref(),
|
||||
mapped_model,
|
||||
provider_api_format,
|
||||
upstream_is_stream,
|
||||
)?;
|
||||
body_json,
|
||||
&format_context,
|
||||
)
|
||||
.ok()?;
|
||||
|
||||
if !apply_local_body_rules(&mut provider_request_body, body_rules, Some(body_json)) {
|
||||
return None;
|
||||
}
|
||||
apply_codex_openai_cli_special_body_edits(
|
||||
apply_codex_openai_responses_special_body_edits(
|
||||
&mut provider_request_body,
|
||||
provider_type,
|
||||
provider_api_format,
|
||||
body_rules,
|
||||
user_api_key_id,
|
||||
);
|
||||
apply_openai_compact_special_body_edits(&mut provider_request_body, provider_api_format);
|
||||
apply_openai_responses_compact_special_body_edits(
|
||||
&mut provider_request_body,
|
||||
provider_api_format,
|
||||
);
|
||||
Some(provider_request_body)
|
||||
}
|
||||
|
||||
@@ -69,18 +73,22 @@ pub fn build_standard_request_body_from_canonical(
|
||||
mapped_model,
|
||||
upstream_is_stream,
|
||||
),
|
||||
"openai:cli" => convert_openai_chat_request_to_openai_cli_request(
|
||||
canonical_request,
|
||||
mapped_model,
|
||||
upstream_is_stream,
|
||||
false,
|
||||
),
|
||||
"openai:compact" => convert_openai_chat_request_to_openai_cli_request(
|
||||
canonical_request,
|
||||
mapped_model,
|
||||
false,
|
||||
true,
|
||||
),
|
||||
"openai:responses" | "openai:cli" => {
|
||||
convert_openai_chat_request_to_openai_responses_request(
|
||||
canonical_request,
|
||||
mapped_model,
|
||||
upstream_is_stream,
|
||||
false,
|
||||
)
|
||||
}
|
||||
"openai:responses:compact" | "openai:compact" => {
|
||||
convert_openai_chat_request_to_openai_responses_request(
|
||||
canonical_request,
|
||||
mapped_model,
|
||||
false,
|
||||
true,
|
||||
)
|
||||
}
|
||||
"claude:chat" | "claude:cli" => convert_openai_chat_request_to_claude_request(
|
||||
canonical_request,
|
||||
mapped_model,
|
||||
@@ -115,8 +123,8 @@ fn normalize_standard_request_to_openai_chat_request_cow<'a>(
|
||||
) -> Option<Cow<'a, Value>> {
|
||||
match client_api_format.trim().to_ascii_lowercase().as_str() {
|
||||
"openai:chat" => Some(Cow::Borrowed(body_json)),
|
||||
"openai:cli" | "openai:compact" => {
|
||||
normalize_openai_cli_request_to_openai_chat_request(body_json).map(Cow::Owned)
|
||||
"openai:responses" | "openai:cli" | "openai:responses:compact" | "openai:compact" => {
|
||||
normalize_openai_responses_request_to_openai_chat_request(body_json).map(Cow::Owned)
|
||||
}
|
||||
"claude:chat" | "claude:cli" => {
|
||||
normalize_claude_request_to_openai_chat_request(body_json).map(Cow::Owned)
|
||||
@@ -149,7 +157,10 @@ pub fn build_standard_upstream_url(
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::build_standard_request_body;
|
||||
use super::{
|
||||
build_standard_request_body, build_standard_request_body_from_canonical,
|
||||
normalize_standard_request_to_openai_chat_request,
|
||||
};
|
||||
use serde_json::{json, Value};
|
||||
|
||||
const STANDARD_SURFACES: &[&str] = &[
|
||||
@@ -251,6 +262,80 @@ mod tests {
|
||||
])
|
||||
}
|
||||
|
||||
fn legacy_openai_responses_alias_request_body(
|
||||
request: &Value,
|
||||
provider_api_format: &str,
|
||||
upstream_is_stream: bool,
|
||||
) -> Value {
|
||||
let chat_canonical = normalize_standard_request_to_openai_chat_request(
|
||||
request,
|
||||
"openai:cli",
|
||||
"/v1/responses",
|
||||
)
|
||||
.expect("legacy openai responses alias normalization should succeed");
|
||||
build_standard_request_body_from_canonical(
|
||||
&chat_canonical,
|
||||
"mapped-model",
|
||||
provider_api_format,
|
||||
upstream_is_stream,
|
||||
)
|
||||
.expect("legacy openai responses alias target conversion should succeed")
|
||||
}
|
||||
|
||||
fn legacy_openai_chat_request_body(
|
||||
request: &Value,
|
||||
provider_api_format: &str,
|
||||
upstream_is_stream: bool,
|
||||
) -> Value {
|
||||
build_standard_request_body_from_canonical(
|
||||
request,
|
||||
"mapped-model",
|
||||
provider_api_format,
|
||||
upstream_is_stream,
|
||||
)
|
||||
.expect("legacy openai chat target conversion should succeed")
|
||||
}
|
||||
|
||||
fn legacy_claude_request_body(
|
||||
request: &Value,
|
||||
provider_api_format: &str,
|
||||
upstream_is_stream: bool,
|
||||
) -> Value {
|
||||
let chat_canonical = normalize_standard_request_to_openai_chat_request(
|
||||
request,
|
||||
"claude:chat",
|
||||
"/v1/messages",
|
||||
)
|
||||
.expect("legacy claude normalization should succeed");
|
||||
build_standard_request_body_from_canonical(
|
||||
&chat_canonical,
|
||||
"mapped-model",
|
||||
provider_api_format,
|
||||
upstream_is_stream,
|
||||
)
|
||||
.expect("legacy claude target conversion should succeed")
|
||||
}
|
||||
|
||||
fn legacy_gemini_request_body(
|
||||
request: &Value,
|
||||
provider_api_format: &str,
|
||||
upstream_is_stream: bool,
|
||||
) -> Value {
|
||||
let chat_canonical = normalize_standard_request_to_openai_chat_request(
|
||||
request,
|
||||
"gemini:chat",
|
||||
"/v1beta/models/source-model:generateContent",
|
||||
)
|
||||
.expect("legacy gemini normalization should succeed");
|
||||
build_standard_request_body_from_canonical(
|
||||
&chat_canonical,
|
||||
"mapped-model",
|
||||
provider_api_format,
|
||||
upstream_is_stream,
|
||||
)
|
||||
.expect("legacy gemini target conversion should succeed")
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn builds_request_body_for_all_standard_surface_pairs_in_sync_and_stream_modes() {
|
||||
for client_api_format in STANDARD_SURFACES {
|
||||
@@ -285,7 +370,357 @@ mod tests {
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn applies_codex_body_rules_for_all_standard_sources_to_openai_cli() {
|
||||
fn openai_responses_request_uses_typed_canonical_without_changing_target_payloads() {
|
||||
let request = json!({
|
||||
"model": "gpt-5",
|
||||
"instructions": "Be exact.",
|
||||
"input": [
|
||||
{
|
||||
"type": "message",
|
||||
"role": "user",
|
||||
"content": [
|
||||
{"type": "input_text", "text": "Inspect this"},
|
||||
{
|
||||
"type": "input_image",
|
||||
"image_url": "data:image/png;base64,iVBORw0KGgo=",
|
||||
"detail": "high"
|
||||
},
|
||||
{
|
||||
"type": "input_file",
|
||||
"file_data": "data:application/pdf;base64,JVBERi0x",
|
||||
"filename": "spec.pdf"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"type": "function_call",
|
||||
"call_id": "call_123",
|
||||
"name": "lookup",
|
||||
"arguments": "{\"q\":\"rust\"}"
|
||||
},
|
||||
{
|
||||
"type": "function_call_output",
|
||||
"call_id": "call_123",
|
||||
"output": "{\"ok\":true}"
|
||||
}
|
||||
],
|
||||
"max_output_tokens": 64,
|
||||
"temperature": 0.2,
|
||||
"top_p": 0.9,
|
||||
"parallel_tool_calls": true,
|
||||
"tools": [{
|
||||
"type": "function",
|
||||
"name": "lookup",
|
||||
"description": "Lookup data",
|
||||
"parameters": {"type": "object"}
|
||||
}],
|
||||
"tool_choice": {"type": "function", "name": "lookup"},
|
||||
"reasoning": {"effort": "high"},
|
||||
"text": {
|
||||
"format": {
|
||||
"type": "json_schema",
|
||||
"json_schema": {"name": "answer", "schema": {"type": "object"}}
|
||||
},
|
||||
"verbosity": "low"
|
||||
},
|
||||
"metadata": {"trace": "abc"}
|
||||
});
|
||||
|
||||
for provider_api_format in STANDARD_SURFACES {
|
||||
for upstream_is_stream in [false, true] {
|
||||
let converted = build_standard_request_body(
|
||||
&request,
|
||||
"openai:cli",
|
||||
"mapped-model",
|
||||
"custom",
|
||||
provider_api_format,
|
||||
"/v1/responses",
|
||||
upstream_is_stream,
|
||||
None,
|
||||
None,
|
||||
)
|
||||
.expect("typed canonical route should build");
|
||||
let legacy = legacy_openai_responses_alias_request_body(
|
||||
&request,
|
||||
provider_api_format,
|
||||
upstream_is_stream,
|
||||
);
|
||||
assert_eq!(
|
||||
converted, legacy,
|
||||
"typed canonical openai:cli -> {provider_api_format} changed payload with upstream_is_stream={upstream_is_stream}"
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn openai_chat_request_uses_typed_canonical_without_changing_target_payloads() {
|
||||
let request = json!({
|
||||
"model": "gpt-5",
|
||||
"messages": [
|
||||
{"role": "system", "content": "Be exact."},
|
||||
{
|
||||
"role": "user",
|
||||
"content": [
|
||||
{"type": "text", "text": "Inspect this"},
|
||||
{
|
||||
"type": "image_url",
|
||||
"image_url": {"url": "data:image/png;base64,iVBORw0KGgo="}
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"role": "assistant",
|
||||
"content": null,
|
||||
"reasoning_parts": [{
|
||||
"type": "thinking",
|
||||
"thinking": "plan",
|
||||
"signature": "sig_123"
|
||||
}],
|
||||
"tool_calls": [{
|
||||
"id": "call_123",
|
||||
"type": "function",
|
||||
"function": {
|
||||
"name": "lookup",
|
||||
"arguments": "{\"q\":\"rust\"}"
|
||||
}
|
||||
}]
|
||||
},
|
||||
{
|
||||
"role": "tool",
|
||||
"tool_call_id": "call_123",
|
||||
"content": {"ok": true}
|
||||
}
|
||||
],
|
||||
"max_completion_tokens": 64,
|
||||
"temperature": 0.2,
|
||||
"tools": [{
|
||||
"type": "function",
|
||||
"function": {
|
||||
"name": "lookup",
|
||||
"description": "Lookup data",
|
||||
"parameters": {"type": "object"}
|
||||
}
|
||||
}],
|
||||
"tool_choice": {"type": "function", "function": {"name": "lookup"}},
|
||||
"reasoning_effort": "medium",
|
||||
"response_format": {
|
||||
"type": "json_schema",
|
||||
"json_schema": {"name": "answer", "schema": {"type": "object"}}
|
||||
}
|
||||
});
|
||||
|
||||
for provider_api_format in STANDARD_SURFACES {
|
||||
for upstream_is_stream in [false, true] {
|
||||
let converted = build_standard_request_body(
|
||||
&request,
|
||||
"openai:chat",
|
||||
"mapped-model",
|
||||
"custom",
|
||||
provider_api_format,
|
||||
"/v1/chat/completions",
|
||||
upstream_is_stream,
|
||||
None,
|
||||
None,
|
||||
)
|
||||
.expect("typed canonical openai chat route should build");
|
||||
let legacy = legacy_openai_chat_request_body(
|
||||
&request,
|
||||
provider_api_format,
|
||||
upstream_is_stream,
|
||||
);
|
||||
assert_eq!(
|
||||
converted, legacy,
|
||||
"typed canonical openai:chat -> {provider_api_format} changed payload with upstream_is_stream={upstream_is_stream}"
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn claude_request_uses_typed_canonical_without_changing_non_claude_target_payloads() {
|
||||
let request = json!({
|
||||
"model": "claude-sonnet-4-5",
|
||||
"system": "Be exact.",
|
||||
"messages": [
|
||||
{
|
||||
"role": "user",
|
||||
"content": [
|
||||
{"type": "text", "text": "Inspect this"},
|
||||
{
|
||||
"type": "image",
|
||||
"source": {
|
||||
"type": "base64",
|
||||
"media_type": "image/png",
|
||||
"data": "iVBORw0KGgo="
|
||||
}
|
||||
},
|
||||
{
|
||||
"type": "document",
|
||||
"source": {
|
||||
"type": "base64",
|
||||
"media_type": "application/pdf",
|
||||
"data": "JVBERi0x"
|
||||
}
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"role": "assistant",
|
||||
"content": [
|
||||
{
|
||||
"type": "thinking",
|
||||
"thinking": "plan",
|
||||
"signature": "sig_123"
|
||||
},
|
||||
{
|
||||
"type": "tool_use",
|
||||
"id": "toolu_123",
|
||||
"name": "lookup",
|
||||
"input": {"q": "rust"}
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"role": "user",
|
||||
"content": [{
|
||||
"type": "tool_result",
|
||||
"tool_use_id": "toolu_123",
|
||||
"content": {"ok": true}
|
||||
}]
|
||||
}
|
||||
],
|
||||
"max_tokens": 64,
|
||||
"temperature": 0.2,
|
||||
"top_p": 0.9,
|
||||
"tools": [{
|
||||
"name": "lookup",
|
||||
"description": "Lookup data",
|
||||
"input_schema": {"type": "object"}
|
||||
}],
|
||||
"tool_choice": {
|
||||
"type": "tool",
|
||||
"name": "lookup",
|
||||
"disable_parallel_tool_use": false
|
||||
},
|
||||
"metadata": {"trace": "abc"},
|
||||
"thinking": {"type": "enabled", "budget_tokens": 2048}
|
||||
});
|
||||
|
||||
for provider_api_format in [
|
||||
"openai:chat",
|
||||
"openai:cli",
|
||||
"openai:compact",
|
||||
"gemini:chat",
|
||||
"gemini:cli",
|
||||
] {
|
||||
for upstream_is_stream in [false, true] {
|
||||
let converted = build_standard_request_body(
|
||||
&request,
|
||||
"claude:chat",
|
||||
"mapped-model",
|
||||
"custom",
|
||||
provider_api_format,
|
||||
"/v1/messages",
|
||||
upstream_is_stream,
|
||||
None,
|
||||
None,
|
||||
)
|
||||
.expect("typed canonical claude route should build");
|
||||
let legacy =
|
||||
legacy_claude_request_body(&request, provider_api_format, upstream_is_stream);
|
||||
assert_eq!(
|
||||
converted, legacy,
|
||||
"typed canonical claude:chat -> {provider_api_format} changed payload with upstream_is_stream={upstream_is_stream}"
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn gemini_request_uses_typed_canonical_without_changing_non_gemini_target_payloads() {
|
||||
let request = json!({
|
||||
"systemInstruction": {
|
||||
"parts": [{"text": "Be exact."}]
|
||||
},
|
||||
"contents": [
|
||||
{
|
||||
"role": "user",
|
||||
"parts": [
|
||||
{"text": "Inspect this"},
|
||||
{"inlineData": {"mimeType": "image/png", "data": "iVBORw0KGgo="}}
|
||||
]
|
||||
},
|
||||
{
|
||||
"role": "model",
|
||||
"parts": [
|
||||
{"text": "plan", "thought": true, "thoughtSignature": "sig_123"},
|
||||
{"functionCall": {"id": "call_123", "name": "lookup", "args": {"q": "rust"}}}
|
||||
]
|
||||
},
|
||||
{
|
||||
"role": "user",
|
||||
"parts": [{
|
||||
"functionResponse": {
|
||||
"id": "call_123",
|
||||
"name": "lookup",
|
||||
"response": {"result": {"ok": true}}
|
||||
}
|
||||
}]
|
||||
}
|
||||
],
|
||||
"generationConfig": {
|
||||
"maxOutputTokens": 64,
|
||||
"temperature": 0.2,
|
||||
"thinkingConfig": {"includeThoughts": true, "thinkingBudget": 2048}
|
||||
},
|
||||
"tools": [{
|
||||
"functionDeclarations": [{
|
||||
"name": "lookup",
|
||||
"description": "Lookup data",
|
||||
"parameters": {"type": "object"}
|
||||
}]
|
||||
}],
|
||||
"toolConfig": {
|
||||
"functionCallingConfig": {
|
||||
"mode": "ANY",
|
||||
"allowedFunctionNames": ["lookup"]
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
for provider_api_format in [
|
||||
"openai:chat",
|
||||
"openai:cli",
|
||||
"openai:compact",
|
||||
"claude:chat",
|
||||
"claude:cli",
|
||||
] {
|
||||
for upstream_is_stream in [false, true] {
|
||||
let converted = build_standard_request_body(
|
||||
&request,
|
||||
"gemini:chat",
|
||||
"mapped-model",
|
||||
"custom",
|
||||
provider_api_format,
|
||||
"/v1beta/models/source-model:generateContent",
|
||||
upstream_is_stream,
|
||||
None,
|
||||
None,
|
||||
)
|
||||
.expect("typed canonical gemini route should build");
|
||||
let legacy =
|
||||
legacy_gemini_request_body(&request, provider_api_format, upstream_is_stream);
|
||||
assert_eq!(
|
||||
converted, legacy,
|
||||
"typed canonical gemini:chat -> {provider_api_format} changed payload with upstream_is_stream={upstream_is_stream}"
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn applies_codex_body_rules_for_all_standard_sources_to_openai_responses() {
|
||||
let body_rules = codex_default_body_rules();
|
||||
|
||||
for client_api_format in STANDARD_SURFACES {
|
||||
|
||||
@@ -4,11 +4,17 @@ pub mod family;
|
||||
pub mod gemini;
|
||||
pub mod matrix;
|
||||
pub mod normalize;
|
||||
pub mod openai_cli;
|
||||
pub mod openai_responses;
|
||||
|
||||
#[allow(deprecated)]
|
||||
pub use codex::apply_openai_compact_special_body_edits;
|
||||
#[allow(deprecated)]
|
||||
pub use codex::{
|
||||
apply_codex_openai_cli_special_body_edits, apply_codex_openai_cli_special_headers,
|
||||
apply_openai_compact_special_body_edits, CODEX_OPENAI_IMAGE_DEFAULT_MODEL,
|
||||
};
|
||||
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,
|
||||
};
|
||||
@@ -18,6 +24,6 @@ pub use matrix::{
|
||||
normalize_standard_request_to_openai_chat_request,
|
||||
};
|
||||
pub use normalize::{
|
||||
build_cross_format_openai_chat_request_body, build_cross_format_openai_cli_request_body,
|
||||
build_local_openai_chat_request_body, build_local_openai_cli_request_body,
|
||||
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,
|
||||
};
|
||||
|
||||
@@ -2,8 +2,8 @@ use serde_json::{json, Value};
|
||||
|
||||
use crate::conversion::request::{
|
||||
convert_openai_chat_request_to_claude_request, convert_openai_chat_request_to_gemini_request,
|
||||
convert_openai_chat_request_to_openai_cli_request,
|
||||
normalize_openai_cli_request_to_openai_chat_request,
|
||||
convert_openai_chat_request_to_openai_responses_request,
|
||||
normalize_openai_responses_request_to_openai_chat_request,
|
||||
};
|
||||
use crate::conversion::{request_conversion_kind, RequestConversionKind};
|
||||
|
||||
@@ -56,8 +56,8 @@ pub fn build_cross_format_openai_chat_request_body(
|
||||
mapped_model,
|
||||
upstream_is_stream,
|
||||
),
|
||||
RequestConversionKind::ToOpenAIFamilyCli => {
|
||||
convert_openai_chat_request_to_openai_cli_request(
|
||||
RequestConversionKind::ToOpenAiResponses => {
|
||||
convert_openai_chat_request_to_openai_responses_request(
|
||||
body_json,
|
||||
mapped_model,
|
||||
upstream_is_stream,
|
||||
@@ -68,7 +68,7 @@ pub fn build_cross_format_openai_chat_request_body(
|
||||
}
|
||||
}
|
||||
|
||||
pub fn build_local_openai_cli_request_body(
|
||||
pub fn build_local_openai_responses_request_body(
|
||||
body_json: &Value,
|
||||
mapped_model: &str,
|
||||
require_streaming: bool,
|
||||
@@ -86,14 +86,14 @@ pub fn build_local_openai_cli_request_body(
|
||||
Some(Value::Object(provider_request_body))
|
||||
}
|
||||
|
||||
pub fn build_cross_format_openai_cli_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_cli_request_to_openai_chat_request(body_json)?;
|
||||
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(
|
||||
@@ -101,8 +101,8 @@ pub fn build_cross_format_openai_cli_request_body(
|
||||
mapped_model,
|
||||
upstream_is_stream,
|
||||
),
|
||||
RequestConversionKind::ToOpenAIFamilyCli => {
|
||||
convert_openai_chat_request_to_openai_cli_request(
|
||||
RequestConversionKind::ToOpenAiResponses => {
|
||||
convert_openai_chat_request_to_openai_responses_request(
|
||||
&chat_like_request,
|
||||
mapped_model,
|
||||
upstream_is_stream,
|
||||
@@ -124,8 +124,10 @@ pub fn build_cross_format_openai_cli_request_body(
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::build_local_openai_cli_request_body;
|
||||
use super::{build_cross_format_openai_cli_request_body, build_local_openai_chat_request_body};
|
||||
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> {
|
||||
@@ -138,20 +140,20 @@ mod tests {
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn builds_openai_chat_cross_format_request_body_from_openai_cli_source() {
|
||||
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_cli_request_body(
|
||||
let provider_request_body = build_cross_format_openai_responses_request_body(
|
||||
&body_json,
|
||||
"gpt-5-upstream",
|
||||
"openai:cli",
|
||||
"openai:responses",
|
||||
"openai:chat",
|
||||
false,
|
||||
)
|
||||
.expect("openai cli to openai chat body should build");
|
||||
.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");
|
||||
@@ -159,7 +161,7 @@ mod tests {
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn local_openai_cli_request_body_preserves_original_field_order() {
|
||||
fn local_openai_responses_request_body_preserves_original_field_order() {
|
||||
let body_json: Value = serde_json::from_str(
|
||||
r#"{
|
||||
"model": "gpt-5",
|
||||
@@ -171,8 +173,8 @@ mod tests {
|
||||
.expect("request json should parse");
|
||||
|
||||
let provider_request_body =
|
||||
build_local_openai_cli_request_body(&body_json, "gpt-5-upstream", false)
|
||||
.expect("openai cli body should build");
|
||||
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),
|
||||
|
||||
@@ -1,76 +0,0 @@
|
||||
use crate::contracts::{
|
||||
OPENAI_CLI_STREAM_PLAN_KIND, OPENAI_CLI_SYNC_PLAN_KIND, OPENAI_COMPACT_STREAM_PLAN_KIND,
|
||||
OPENAI_COMPACT_SYNC_PLAN_KIND,
|
||||
};
|
||||
|
||||
#[derive(Debug, Clone, Copy)]
|
||||
pub struct LocalOpenAiCliSpec {
|
||||
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<LocalOpenAiCliSpec> {
|
||||
match plan_kind {
|
||||
OPENAI_CLI_SYNC_PLAN_KIND => Some(LocalOpenAiCliSpec {
|
||||
api_format: "openai:cli",
|
||||
decision_kind: OPENAI_CLI_SYNC_PLAN_KIND,
|
||||
report_kind: "openai_cli_sync_success",
|
||||
compact: false,
|
||||
require_streaming: false,
|
||||
}),
|
||||
OPENAI_COMPACT_SYNC_PLAN_KIND => Some(LocalOpenAiCliSpec {
|
||||
api_format: "openai:compact",
|
||||
decision_kind: OPENAI_COMPACT_SYNC_PLAN_KIND,
|
||||
report_kind: "openai_cli_sync_success",
|
||||
compact: true,
|
||||
require_streaming: false,
|
||||
}),
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn resolve_stream_spec(plan_kind: &str) -> Option<LocalOpenAiCliSpec> {
|
||||
match plan_kind {
|
||||
OPENAI_CLI_STREAM_PLAN_KIND => Some(LocalOpenAiCliSpec {
|
||||
api_format: "openai:cli",
|
||||
decision_kind: OPENAI_CLI_STREAM_PLAN_KIND,
|
||||
report_kind: "openai_cli_stream_success",
|
||||
compact: false,
|
||||
require_streaming: true,
|
||||
}),
|
||||
OPENAI_COMPACT_STREAM_PLAN_KIND => Some(LocalOpenAiCliSpec {
|
||||
api_format: "openai:compact",
|
||||
decision_kind: OPENAI_COMPACT_STREAM_PLAN_KIND,
|
||||
report_kind: "openai_cli_stream_success",
|
||||
compact: true,
|
||||
require_streaming: true,
|
||||
}),
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::{resolve_stream_spec, resolve_sync_spec};
|
||||
|
||||
#[test]
|
||||
fn resolves_openai_cli_sync_spec() {
|
||||
let spec = resolve_sync_spec("openai_cli_sync").expect("spec");
|
||||
assert_eq!(spec.api_format, "openai:cli");
|
||||
assert_eq!(spec.report_kind, "openai_cli_sync_success");
|
||||
assert!(!spec.compact);
|
||||
assert!(!spec.require_streaming);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn resolves_openai_compact_stream_spec() {
|
||||
let spec = resolve_stream_spec("openai_compact_stream").expect("spec");
|
||||
assert_eq!(spec.api_format, "openai:compact");
|
||||
assert_eq!(spec.report_kind, "openai_cli_stream_success");
|
||||
assert!(spec.compact);
|
||||
assert!(spec.require_streaming);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,126 @@
|
||||
use crate::contracts::{
|
||||
OPENAI_CLI_STREAM_PLAN_KIND, OPENAI_CLI_SYNC_PLAN_KIND, OPENAI_COMPACT_STREAM_PLAN_KIND,
|
||||
OPENAI_COMPACT_SYNC_PLAN_KIND, 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,
|
||||
}),
|
||||
OPENAI_CLI_SYNC_PLAN_KIND => Some(LocalOpenAiResponsesSpec {
|
||||
api_format: "openai:responses",
|
||||
decision_kind: OPENAI_CLI_SYNC_PLAN_KIND,
|
||||
report_kind: "openai_cli_sync_success",
|
||||
compact: false,
|
||||
require_streaming: false,
|
||||
}),
|
||||
OPENAI_COMPACT_SYNC_PLAN_KIND => Some(LocalOpenAiResponsesSpec {
|
||||
api_format: "openai:responses:compact",
|
||||
decision_kind: OPENAI_COMPACT_SYNC_PLAN_KIND,
|
||||
report_kind: OPENAI_RESPONSES_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,
|
||||
}),
|
||||
OPENAI_CLI_STREAM_PLAN_KIND => Some(LocalOpenAiResponsesSpec {
|
||||
api_format: "openai:responses",
|
||||
decision_kind: OPENAI_CLI_STREAM_PLAN_KIND,
|
||||
report_kind: "openai_cli_stream_success",
|
||||
compact: false,
|
||||
require_streaming: true,
|
||||
}),
|
||||
OPENAI_COMPACT_STREAM_PLAN_KIND => Some(LocalOpenAiResponsesSpec {
|
||||
api_format: "openai:responses:compact",
|
||||
decision_kind: OPENAI_COMPACT_STREAM_PLAN_KIND,
|
||||
report_kind: OPENAI_RESPONSES_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_legacy_openai_cli_sync_spec() {
|
||||
let spec = resolve_sync_spec("openai_cli_sync").expect("spec");
|
||||
assert_eq!(spec.api_format, "openai:responses");
|
||||
assert_eq!(spec.report_kind, "openai_cli_sync_success");
|
||||
assert!(!spec.compact);
|
||||
assert!(!spec.require_streaming);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn resolves_openai_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);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn resolves_legacy_openai_compact_stream_spec() {
|
||||
let spec = resolve_stream_spec("openai_compact_stream").expect("spec");
|
||||
assert_eq!(spec.api_format, "openai:responses:compact");
|
||||
assert_eq!(spec.report_kind, "openai_responses_stream_success");
|
||||
assert!(spec.compact);
|
||||
assert!(spec.require_streaming);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user