migrate ai format conversion to responses adapters

This commit is contained in:
fawney19
2026-04-26 20:32:55 +08:00
parent e36fb8c07a
commit 5b914aa78c
183 changed files with 13539 additions and 3401 deletions

View File

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

View File

@@ -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(

View File

@@ -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",

View File

@@ -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 {

View File

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

View File

@@ -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),

View File

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

View File

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