Merge remote-tracking branch 'origin/main' into codex/usage-pending-reasoning-reset-expiry-20260712

# Conflicts:
#	crates/aether-ai-formats/src/formats/openai/responses/mod.rs
#	crates/aether-usage/runtime/src/runtime.rs
#	frontend/src/features/usage/components/UsageRecordsTable.vue
#	frontend/src/features/usage/components/__tests__/UsageRecordsTable.spec.ts
This commit is contained in:
MMEXA
2026-07-16 23:41:58 +08:00
1027 changed files with 48510 additions and 29027 deletions
+19
View File
@@ -0,0 +1,19 @@
[package]
name = "aether-ai-formats"
version = "0.1.0"
edition.workspace = true
license.workspace = true
repository.workspace = true
description = "Pure AI API format, surface, planning, and finalize logic for Aether"
[dependencies]
aether-contracts.workspace = true
base64.workspace = true
http.workspace = true
regex.workspace = true
serde.workspace = true
serde_json.workspace = true
sha1 = "0.10"
sha2.workspace = true
url.workspace = true
uuid.workspace = true
+307
View File
@@ -0,0 +1,307 @@
pub use crate::contracts::{
core_error_background_report_kind, core_error_default_client_api_format,
core_success_background_report_kind, implicit_sync_finalize_report_kind,
is_openai_responses_stream_plan_kind, is_openai_responses_sync_plan_kind, AiControlPlanRequest,
ExecutionRuntimeAuthContext, CLAUDE_CHAT_STREAM_PLAN_KIND,
CLAUDE_CHAT_STREAM_SUCCESS_REPORT_KIND, CLAUDE_CHAT_SYNC_ERROR_REPORT_KIND,
CLAUDE_CHAT_SYNC_FINALIZE_REPORT_KIND, CLAUDE_CHAT_SYNC_PLAN_KIND,
CLAUDE_CHAT_SYNC_SUCCESS_REPORT_KIND, CLAUDE_CLI_STREAM_PLAN_KIND,
CLAUDE_CLI_STREAM_SUCCESS_REPORT_KIND, CLAUDE_CLI_SYNC_ERROR_REPORT_KIND,
CLAUDE_CLI_SYNC_FINALIZE_REPORT_KIND, CLAUDE_CLI_SYNC_PLAN_KIND,
CLAUDE_CLI_SYNC_SUCCESS_REPORT_KIND, EXECUTION_RUNTIME_STREAM_ACTION,
EXECUTION_RUNTIME_STREAM_DECISION_ACTION, EXECUTION_RUNTIME_SYNC_ACTION,
EXECUTION_RUNTIME_SYNC_DECISION_ACTION, GEMINI_CHAT_STREAM_PLAN_KIND,
GEMINI_CHAT_STREAM_SUCCESS_REPORT_KIND, GEMINI_CHAT_SYNC_ERROR_REPORT_KIND,
GEMINI_CHAT_SYNC_FINALIZE_REPORT_KIND, GEMINI_CHAT_SYNC_PLAN_KIND,
GEMINI_CHAT_SYNC_SUCCESS_REPORT_KIND, GEMINI_CLI_STREAM_PLAN_KIND,
GEMINI_CLI_STREAM_SUCCESS_REPORT_KIND, GEMINI_CLI_SYNC_ERROR_REPORT_KIND,
GEMINI_CLI_SYNC_FINALIZE_REPORT_KIND, GEMINI_CLI_SYNC_PLAN_KIND,
GEMINI_CLI_SYNC_SUCCESS_REPORT_KIND, GEMINI_EMBEDDING_SYNC_PLAN_KIND,
GEMINI_EMBEDDING_SYNC_SUCCESS_REPORT_KIND, GEMINI_FILES_DELETE_PLAN_KIND,
GEMINI_FILES_DOWNLOAD_PLAN_KIND, GEMINI_FILES_GET_PLAN_KIND, GEMINI_FILES_LIST_PLAN_KIND,
GEMINI_FILES_UPLOAD_PLAN_KIND, GEMINI_INTERACTIONS_STREAM_PLAN_KIND,
GEMINI_INTERACTIONS_STREAM_SUCCESS_REPORT_KIND, GEMINI_INTERACTIONS_SYNC_ERROR_REPORT_KIND,
GEMINI_INTERACTIONS_SYNC_FINALIZE_REPORT_KIND, GEMINI_INTERACTIONS_SYNC_PLAN_KIND,
GEMINI_INTERACTIONS_SYNC_SUCCESS_REPORT_KIND, GEMINI_VIDEO_CANCEL_SYNC_PLAN_KIND,
GEMINI_VIDEO_CREATE_SYNC_FINALIZE_REPORT_KIND, GEMINI_VIDEO_CREATE_SYNC_PLAN_KIND,
OPENAI_CHAT_STREAM_PLAN_KIND, OPENAI_CHAT_STREAM_SUCCESS_REPORT_KIND,
OPENAI_CHAT_SYNC_ERROR_REPORT_KIND, OPENAI_CHAT_SYNC_FINALIZE_REPORT_KIND,
OPENAI_CHAT_SYNC_PLAN_KIND, OPENAI_CHAT_SYNC_SUCCESS_REPORT_KIND,
OPENAI_EMBEDDING_SYNC_ERROR_REPORT_KIND, OPENAI_EMBEDDING_SYNC_FINALIZE_REPORT_KIND,
OPENAI_EMBEDDING_SYNC_PLAN_KIND, OPENAI_EMBEDDING_SYNC_SUCCESS_REPORT_KIND,
OPENAI_IMAGE_STREAM_PLAN_KIND, OPENAI_IMAGE_STREAM_SUCCESS_REPORT_KIND,
OPENAI_IMAGE_SYNC_ERROR_REPORT_KIND, OPENAI_IMAGE_SYNC_FINALIZE_REPORT_KIND,
OPENAI_IMAGE_SYNC_PLAN_KIND, OPENAI_IMAGE_SYNC_SUCCESS_REPORT_KIND,
OPENAI_RERANK_SYNC_PLAN_KIND, OPENAI_RESPONSES_COMPACT_STREAM_PLAN_KIND,
OPENAI_RESPONSES_COMPACT_STREAM_SUCCESS_REPORT_KIND,
OPENAI_RESPONSES_COMPACT_SYNC_ERROR_REPORT_KIND,
OPENAI_RESPONSES_COMPACT_SYNC_FINALIZE_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_ERROR_REPORT_KIND,
OPENAI_RESPONSES_SYNC_FINALIZE_REPORT_KIND, OPENAI_RESPONSES_SYNC_PLAN_KIND,
OPENAI_RESPONSES_SYNC_SUCCESS_REPORT_KIND, OPENAI_SEARCH_SYNC_PLAN_KIND,
OPENAI_SEARCH_SYNC_SUCCESS_REPORT_KIND, OPENAI_VIDEO_CANCEL_SYNC_PLAN_KIND,
OPENAI_VIDEO_CONTENT_PLAN_KIND, OPENAI_VIDEO_CREATE_SYNC_FINALIZE_REPORT_KIND,
OPENAI_VIDEO_CREATE_SYNC_PLAN_KIND, OPENAI_VIDEO_DELETE_SYNC_PLAN_KIND,
OPENAI_VIDEO_REMIX_SYNC_PLAN_KIND,
};
pub use crate::formats::claude::messages::stream::{ClaudeClientEmitter, ClaudeProviderState};
pub use crate::formats::gemini::generate_content::stream::{
GeminiClientEmitter, GeminiProviderState,
};
pub use crate::formats::openai::chat::stream::{
OpenAIChatClientEmitter, OpenAIChatProviderState, OpenAIResponsesClientEmitter,
OpenAIResponsesProviderState,
};
pub use crate::formats::openai::image::stream::{
maybe_build_openai_image_sync_finalize_product, OpenAiImageStreamState,
OpenAiImageSyncFinalizeProduct,
};
pub use crate::formats::openai::prompt_cache::resolve_openai_prompt_cache_ttl_minutes;
pub use crate::formats::openai::shared::{
copy_request_number_field, copy_request_number_field_as,
map_openai_reasoning_effort_to_claude_output, map_openai_reasoning_effort_to_gemini_budget,
parse_openai_stop_sequences, resolve_openai_chat_max_tokens, value_as_u64,
};
pub use crate::formats::openai::{
reasoning::{
validate_openai_reasoning_request, OpenAiReasoningContractViolation,
OpenAiReasoningViolationKind,
},
request_contract::{
finalize_openai_provider_request,
finalize_openai_provider_request_with_codex_model_capabilities,
validate_openai_provider_request_contract, OpenAiProviderRequestContractViolation,
OpenAiProviderRequestFinalization,
},
};
pub use crate::formats::shared::error_body::{
build_core_error_body_for_client_format, is_core_error_finalize_kind, LocalCoreSyncErrorKind,
};
pub use crate::formats::shared::image_bridge::{
build_gemini_image_request_body_from_openai_image_request,
build_gemini_image_response_from_openai_image_response,
build_gemini_image_response_from_openai_responses_image_response,
build_openai_image_provider_body_from_response_stream_sync_body,
build_openai_image_request_body_from_gemini_image_request,
build_openai_image_response_from_gemini_response,
build_openai_image_response_from_response_stream_sync_body, gemini_request_is_image_generation,
resolve_requested_gemini_image_model_for_request, GeminiImageRequestForOpenAi,
OpenAiImageRequestForGemini,
};
pub use crate::formats::shared::model_directives::{
apply_model_directive_mapping_patch, apply_model_directive_overrides_from_model,
apply_model_directive_overrides_from_request, claude_model_uses_adaptive_effort,
default_model_directive_mapping_patch, default_model_directive_suffixes,
default_model_directives_config, extract_gemini_model_from_path,
gemini_model_uses_thinking_level, model_directive_base_model,
model_directive_builtin_suffix_supported_for_source_model,
model_directive_suffix_has_builtin_mapping, normalize_model_directive_model,
openai_model_supports_prompt_cache_options, parse_model_directive,
parse_model_directive_with_suffixes, reasoning_effort_supported_for_model, ModelDirective,
ModelDirectiveSuffixResolution, ModelOverride, ReasoningEffort, ServiceTier,
CROSS_PROVIDER_MODEL_DIRECTIVE_SUFFIXES, MODEL_DIRECTIVE_API_FORMATS,
OPENAI_MODEL_DIRECTIVE_SUFFIXES,
};
pub use crate::formats::shared::passthrough::{
resolve_stream_spec as resolve_local_same_format_stream_spec,
resolve_sync_spec as resolve_local_same_format_sync_spec, LocalSameFormatProviderFamily,
LocalSameFormatProviderSpec,
};
pub use crate::formats::shared::request::{
endpoint_config_forces_upstream_stream_policy, enforce_request_body_stream_field,
forbid_upstream_streaming_for_provider, force_upstream_streaming_for_provider,
parse_direct_request_body, resolve_upstream_is_stream_for_provider,
resolve_upstream_is_stream_from_endpoint_config,
};
pub use crate::formats::shared::request_matrix::{
build_standard_request_body_from_canonical,
build_standard_request_body_from_canonical_with_model_directives,
};
pub use crate::formats::shared::response::{
build_generated_tool_call_id, build_local_success_background_report,
build_local_success_conversion_background_report, canonicalize_tool_arguments,
prepare_local_success_response_parts, prepare_local_success_response_parts_owned,
LocalSyncReportParts,
};
pub use crate::formats::shared::routing::{
is_matching_stream_http_request, is_matching_stream_request,
request_path_implies_stream_request, resolve_execution_runtime_stream_plan_kind,
resolve_execution_runtime_sync_plan_kind, sanitize_request_path,
sanitize_request_path_and_query, sanitize_request_query_string,
supports_stream_execution_decision_kind, supports_sync_execution_decision_kind,
};
pub use crate::formats::shared::sse::{encode_done_sse, encode_json_sse, map_claude_stop_reason};
pub use crate::formats::shared::standard_matrix::normalize_standard_request_to_openai_chat_request;
pub use crate::formats::shared::stream_core::common::*;
pub use crate::formats::shared::stream_core::{
CanonicalStreamFrame, StreamingStandardFormatMatrix, StreamingStandardTerminalObserver,
};
pub use crate::formats::shared::sync_products::{
aggregate_claude_stream_sync_response, aggregate_gemini_stream_sync_response,
aggregate_openai_chat_stream_sync_response, aggregate_openai_responses_stream_sync_response,
aggregate_standard_chat_stream_sync_response, aggregate_standard_cli_stream_sync_response,
convert_standard_chat_response, convert_standard_cli_response,
maybe_build_openai_chat_cross_format_sync_product_from_normalized_payload,
maybe_build_openai_responses_cross_format_sync_product_from_normalized_payload,
maybe_build_openai_responses_same_family_sync_body_from_normalized_payload,
maybe_build_standard_cross_format_sync_product,
maybe_build_standard_cross_format_sync_product_from_normalized_payload,
maybe_build_standard_same_format_sync_body_from_normalized_payload,
maybe_build_standard_sync_finalize_product_from_normalized_payload,
StandardCrossFormatSyncProduct, StandardSyncFinalizeNormalizedProduct,
};
pub use crate::formats::shared::sync_to_stream::{
maybe_bridge_standard_sync_json_to_stream, SyncToStreamBridgeOutcome,
};
pub use crate::formats::shared::{
maybe_build_ai_surface_stream_rewriter, resolve_finalize_stream_rewrite_mode,
AiSurfaceFinalizeError, AiSurfaceStreamRewriter, FinalizeStreamRewriteMode,
};
pub use crate::formats::{
claude::messages::{
resolve_stream_spec as resolve_claude_stream_spec,
resolve_sync_spec as resolve_claude_sync_spec,
},
gemini::generate_content::{
resolve_stream_spec as resolve_gemini_stream_spec,
resolve_sync_spec as resolve_gemini_sync_spec,
},
openai::{
embedding::spec::resolve_sync_spec as resolve_openai_embedding_sync_spec,
responses::{
codex::{
apply_codex_openai_compact_terminal_headers,
apply_codex_openai_responses_chat_body_edits,
apply_codex_openai_responses_lite_header_for_request_body_with_capabilities,
apply_codex_openai_responses_lite_header_with_capabilities,
apply_codex_openai_responses_special_body_edits,
apply_codex_openai_responses_special_body_edits_with_source_model_and_capabilities,
apply_codex_openai_special_headers,
apply_openai_responses_compact_special_body_edits,
build_codex_model_catalog_metadata, parse_codex_auth_identity,
resolve_codex_responses_model_capabilities, CodexAuthIdentity,
CodexResponsesModelCapabilities, CODEX_OPENAI_IMAGE_DEFAULT_MODEL,
CODEX_OPENAI_IMAGE_DEFAULT_OUTPUT_FORMAT,
CODEX_OPENAI_IMAGE_DEFAULT_VARIATION_MODEL,
CODEX_OPENAI_IMAGE_DEFAULT_VARIATION_PROMPT, CODEX_OPENAI_IMAGE_INTERNAL_MODEL,
},
spec::{
resolve_stream_spec as resolve_openai_responses_stream_spec,
resolve_sync_spec as resolve_openai_responses_sync_spec, LocalOpenAiResponsesSpec,
},
},
},
shared::{
family::{LocalStandardSourceFamily, LocalStandardSourceMode, LocalStandardSpec},
standard_matrix::{
build_standard_request_body, build_standard_request_body_with_model_directives,
build_standard_request_body_with_model_directives_and_request_headers,
},
standard_normalize::{
build_cross_format_openai_chat_request_body,
build_cross_format_openai_chat_request_body_with_model_directives,
build_cross_format_openai_responses_request_body,
build_cross_format_openai_responses_request_body_with_model_directives,
build_local_openai_chat_request_body,
build_local_openai_chat_request_body_with_model_directives,
build_local_openai_responses_request_body,
build_local_openai_responses_request_body_with_model_directives,
is_claude_messages_shaped_body_on_openai_chat_endpoint,
},
},
};
pub use crate::formats::{
gemini::files::spec::{
resolve_stream_spec as resolve_gemini_files_stream_spec,
resolve_sync_spec as resolve_gemini_files_sync_spec, LocalGeminiFilesSpec,
},
openai::image::{
request::{
build_chatgpt_web_image_request_body,
build_codex_openai_image_api_provider_request_body,
build_openai_image_api_provider_request_body, build_openai_image_provider_request_body,
default_model_for_openai_image_operation, is_openai_image_stream_request,
normalize_openai_image_quality, normalize_openai_image_request,
normalize_openai_image_request_with_options, openai_image_operation_from_path,
project_codex_openai_image_api_request_body, project_openai_image_api_request_body,
resolve_requested_openai_image_model_for_request, ChatGptWebImageRequestError,
NormalizedOpenAiImageRequest, OpenAiImageNormalizeOptions, OpenAiImageOperation,
OpenAiImageResponseFormat,
},
spec::{
resolve_stream_spec as resolve_local_image_stream_spec,
resolve_sync_spec as resolve_local_image_sync_spec, LocalOpenAiImageSpec,
},
},
shared::video::{
resolve_sync_spec as resolve_local_video_sync_spec, LocalVideoCreateFamily,
LocalVideoCreateSpec,
},
};
pub use crate::provider_compat::kiro_stream::{
build_kiro_final_message_sse_events, build_kiro_initial_sse_events,
build_kiro_stream_error_sse_events, calculate_kiro_context_input_tokens,
encode_kiro_sse_events, estimate_kiro_tokens, find_kiro_real_thinking_end_tag,
find_kiro_real_thinking_end_tag_at_buffer_end, find_kiro_real_thinking_start_tag, kiro_crc32,
KiroToClaudeCliStreamState, KIRO_MAX_THINKING_BUFFER,
};
pub use crate::provider_compat::private_envelope::{
extract_provider_private_stream_error_body, maybe_build_provider_private_stream_normalizer,
normalize_provider_private_report_context, normalize_provider_private_response_value,
provider_private_response_allows_sync_finalize, stream_body_contains_error_event,
transform_provider_private_stream_line, ProviderPrivateStreamNormalizer,
};
pub use crate::provider_compat::surfaces::{
provider_adaptation_allows_sync_finalize_envelope, provider_adaptation_anchor_api_format,
provider_adaptation_descriptor_for_envelope, provider_adaptation_descriptor_for_provider_type,
provider_adaptation_requires_eventstream_accept,
provider_adaptation_should_unwrap_stream_envelope, ProviderAdaptationDescriptor,
ProviderAdaptationSurface, ANTIGRAVITY_V1INTERNAL_ENVELOPE_NAME,
GEMINI_CLI_V1INTERNAL_ENVELOPE_NAME, KIRO_ENVELOPE_NAME,
};
pub use aether_ai_formats::formats::conversion::request::{
convert_openai_chat_request_to_claude_request, convert_openai_chat_request_to_gemini_request,
convert_openai_chat_request_to_openai_responses_request, extract_openai_text_content,
normalize_claude_request_to_openai_chat_request,
normalize_gemini_request_to_openai_chat_request,
normalize_openai_responses_request_to_openai_chat_request, parse_openai_tool_result_content,
};
pub use aether_ai_formats::formats::conversion::response::{
build_openai_responses_response, build_openai_responses_response_with_content,
build_openai_responses_response_with_reasoning, convert_claude_chat_response_to_openai_chat,
convert_claude_response_to_openai_responses, convert_gemini_chat_response_to_openai_chat,
convert_gemini_response_to_openai_responses, convert_openai_chat_response_to_claude_chat,
convert_openai_chat_response_to_gemini_chat, convert_openai_chat_response_to_openai_responses,
convert_openai_responses_response_to_openai_chat, OpenAiResponsesResponseUsage,
};
pub use aether_ai_formats::{
api_format_alias_matches, api_format_permission_covers, api_format_permission_storage_aliases,
api_format_storage_aliases, intersect_api_format_allowed_lists,
is_openai_responses_compact_format, is_openai_responses_family_format,
is_openai_responses_format, normalize_api_format_alias,
};
pub use aether_ai_formats::{
canonical_request_unknown_block_count, canonical_response_unknown_block_count,
canonical_to_claude_request, canonical_to_claude_response, canonical_to_gemini_request,
canonical_to_gemini_response, canonical_to_openai_chat_request,
canonical_to_openai_chat_response, canonical_to_openai_responses_compact_request,
canonical_to_openai_responses_compact_response, canonical_to_openai_responses_request,
canonical_to_openai_responses_response, canonical_unknown_block_count, convert_request,
convert_request_pure, convert_request_pure_with_context, convert_response,
convert_response_pure, emit_request_pure, emit_response_pure, from_claude_to_canonical_request,
from_claude_to_canonical_response, from_gemini_to_canonical_request,
from_gemini_to_canonical_response, from_openai_chat_to_canonical_request,
from_openai_chat_to_canonical_response, from_openai_responses_to_canonical_request,
from_openai_responses_to_canonical_response, is_gemini_interactions_api_format,
parse_request_pure, parse_response_pure, request_candidate_api_format_preference,
request_candidate_api_formats, request_conversion_kind,
request_conversion_requires_enable_flag, sync_chat_response_conversion_kind,
sync_cli_response_conversion_kind, CanonicalContentBlock, CanonicalGenerationConfig,
CanonicalInstruction, CanonicalMessage, CanonicalRequest, CanonicalResponse,
CanonicalResponseFormat, CanonicalResponseOutput, CanonicalRole, CanonicalStopReason,
CanonicalThinkingConfig, CanonicalToolChoice, CanonicalToolDefinition, CanonicalUsage,
ConversionFieldRecord, ConversionFieldStatus, ConversionReport, Converted, FormatContext,
FormatError, FormatFamily, FormatId, FormatProfile, RequestConversionKind,
SyncChatResponseConversionKind, SyncCliResponseConversionKind,
};
@@ -0,0 +1,4 @@
pub const EXECUTION_RUNTIME_SYNC_ACTION: &str = "execution_runtime_sync";
pub const EXECUTION_RUNTIME_SYNC_DECISION_ACTION: &str = "execution_runtime_sync_decision";
pub const EXECUTION_RUNTIME_STREAM_ACTION: &str = "execution_runtime_stream";
pub const EXECUTION_RUNTIME_STREAM_DECISION_ACTION: &str = "execution_runtime_stream_decision";
@@ -0,0 +1,19 @@
use serde::{Deserialize, Serialize};
fn is_false(value: &bool) -> bool {
!*value
}
#[derive(Debug, Clone, Deserialize, Serialize)]
pub struct ExecutionRuntimeAuthContext {
pub user_id: String,
pub api_key_id: String,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub username: Option<String>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub api_key_name: Option<String>,
pub balance_remaining: Option<f64>,
pub access_allowed: bool,
#[serde(default, skip_serializing_if = "is_false")]
pub api_key_is_standalone: bool,
}
@@ -0,0 +1,88 @@
use std::collections::BTreeMap;
use serde::Serialize;
use crate::contracts::ExecutionRuntimeAuthContext;
#[derive(Debug, Serialize)]
pub struct AiControlPlanRequest {
pub trace_id: String,
pub method: String,
pub path: String,
pub query_string: Option<String>,
pub headers: BTreeMap<String, String>,
pub body_json: serde_json::Value,
#[serde(skip_serializing_if = "Option::is_none")]
pub body_base64: Option<String>,
pub auth_context: Option<ExecutionRuntimeAuthContext>,
}
#[allow(clippy::too_many_arguments)]
pub fn build_ai_control_plan_request(
trace_id: &str,
method: &str,
path: &str,
query_string: Option<&str>,
headers: BTreeMap<String, String>,
body_json: serde_json::Value,
body_base64: Option<String>,
auth_context: Option<ExecutionRuntimeAuthContext>,
) -> AiControlPlanRequest {
AiControlPlanRequest {
trace_id: trace_id.to_string(),
method: method.to_string(),
path: path.to_string(),
query_string: query_string.map(ToOwned::to_owned),
headers,
body_json,
body_base64,
auth_context,
}
}
#[cfg(test)]
mod tests {
use std::collections::BTreeMap;
use super::{build_ai_control_plan_request, ExecutionRuntimeAuthContext};
#[test]
fn build_ai_control_plan_request_preserves_request_shape() {
let payload = build_ai_control_plan_request(
"trace-123",
"POST",
"/v1/chat/completions",
Some("stream=true"),
BTreeMap::from([("content-type".to_string(), "application/json".to_string())]),
serde_json::json!({"model": "gpt-5"}),
Some("eyJmb28iOiJiYXIifQ==".to_string()),
Some(ExecutionRuntimeAuthContext {
user_id: "user-1".to_string(),
api_key_id: "key-1".to_string(),
username: None,
api_key_name: None,
balance_remaining: Some(12.5),
access_allowed: true,
api_key_is_standalone: false,
}),
);
assert_eq!(payload.trace_id, "trace-123");
assert_eq!(payload.method, "POST");
assert_eq!(payload.path, "/v1/chat/completions");
assert_eq!(payload.query_string.as_deref(), Some("stream=true"));
assert_eq!(
payload.headers.get("content-type").map(String::as_str),
Some("application/json")
);
assert_eq!(payload.body_json, serde_json::json!({"model": "gpt-5"}));
assert_eq!(payload.body_base64.as_deref(), Some("eyJmb28iOiJiYXIifQ=="));
assert_eq!(
payload
.auth_context
.as_ref()
.map(|ctx| ctx.user_id.as_str()),
Some("user-1")
);
}
}
@@ -0,0 +1,57 @@
mod actions;
mod auth_context;
mod control_payloads;
mod plan_kinds;
mod report_kinds;
pub use actions::{
EXECUTION_RUNTIME_STREAM_ACTION, EXECUTION_RUNTIME_STREAM_DECISION_ACTION,
EXECUTION_RUNTIME_SYNC_ACTION, EXECUTION_RUNTIME_SYNC_DECISION_ACTION,
};
pub use auth_context::ExecutionRuntimeAuthContext;
pub use control_payloads::{build_ai_control_plan_request, AiControlPlanRequest};
pub use plan_kinds::{
is_openai_responses_stream_plan_kind, is_openai_responses_sync_plan_kind,
CLAUDE_CHAT_STREAM_PLAN_KIND, CLAUDE_CHAT_SYNC_PLAN_KIND, CLAUDE_CLI_STREAM_PLAN_KIND,
CLAUDE_CLI_SYNC_PLAN_KIND, GEMINI_CHAT_STREAM_PLAN_KIND, GEMINI_CHAT_SYNC_PLAN_KIND,
GEMINI_CLI_STREAM_PLAN_KIND, GEMINI_CLI_SYNC_PLAN_KIND, GEMINI_EMBEDDING_SYNC_PLAN_KIND,
GEMINI_FILES_DELETE_PLAN_KIND, GEMINI_FILES_DOWNLOAD_PLAN_KIND, GEMINI_FILES_GET_PLAN_KIND,
GEMINI_FILES_LIST_PLAN_KIND, GEMINI_FILES_UPLOAD_PLAN_KIND,
GEMINI_INTERACTIONS_STREAM_PLAN_KIND, GEMINI_INTERACTIONS_SYNC_PLAN_KIND,
GEMINI_VIDEO_CANCEL_SYNC_PLAN_KIND, GEMINI_VIDEO_CREATE_SYNC_PLAN_KIND,
OPENAI_CHAT_STREAM_PLAN_KIND, OPENAI_CHAT_SYNC_PLAN_KIND, OPENAI_EMBEDDING_SYNC_PLAN_KIND,
OPENAI_IMAGE_STREAM_PLAN_KIND, OPENAI_IMAGE_SYNC_PLAN_KIND, OPENAI_RERANK_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_SEARCH_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,
};
pub use report_kinds::{
core_error_background_report_kind, core_error_default_client_api_format,
core_success_background_report_kind, implicit_stream_success_report_kind,
implicit_sync_finalize_report_kind, CLAUDE_CHAT_STREAM_SUCCESS_REPORT_KIND,
CLAUDE_CHAT_SYNC_ERROR_REPORT_KIND, CLAUDE_CHAT_SYNC_FINALIZE_REPORT_KIND,
CLAUDE_CHAT_SYNC_SUCCESS_REPORT_KIND, CLAUDE_CLI_STREAM_SUCCESS_REPORT_KIND,
CLAUDE_CLI_SYNC_ERROR_REPORT_KIND, CLAUDE_CLI_SYNC_FINALIZE_REPORT_KIND,
CLAUDE_CLI_SYNC_SUCCESS_REPORT_KIND, GEMINI_CHAT_STREAM_SUCCESS_REPORT_KIND,
GEMINI_CHAT_SYNC_ERROR_REPORT_KIND, GEMINI_CHAT_SYNC_FINALIZE_REPORT_KIND,
GEMINI_CHAT_SYNC_SUCCESS_REPORT_KIND, GEMINI_CLI_STREAM_SUCCESS_REPORT_KIND,
GEMINI_CLI_SYNC_ERROR_REPORT_KIND, GEMINI_CLI_SYNC_FINALIZE_REPORT_KIND,
GEMINI_CLI_SYNC_SUCCESS_REPORT_KIND, GEMINI_EMBEDDING_SYNC_SUCCESS_REPORT_KIND,
GEMINI_INTERACTIONS_STREAM_SUCCESS_REPORT_KIND, GEMINI_INTERACTIONS_SYNC_ERROR_REPORT_KIND,
GEMINI_INTERACTIONS_SYNC_FINALIZE_REPORT_KIND, GEMINI_INTERACTIONS_SYNC_SUCCESS_REPORT_KIND,
GEMINI_VIDEO_CREATE_SYNC_FINALIZE_REPORT_KIND, OPENAI_CHAT_STREAM_SUCCESS_REPORT_KIND,
OPENAI_CHAT_SYNC_ERROR_REPORT_KIND, OPENAI_CHAT_SYNC_FINALIZE_REPORT_KIND,
OPENAI_CHAT_SYNC_SUCCESS_REPORT_KIND, OPENAI_EMBEDDING_SYNC_ERROR_REPORT_KIND,
OPENAI_EMBEDDING_SYNC_FINALIZE_REPORT_KIND, OPENAI_EMBEDDING_SYNC_SUCCESS_REPORT_KIND,
OPENAI_IMAGE_STREAM_SUCCESS_REPORT_KIND, OPENAI_IMAGE_SYNC_ERROR_REPORT_KIND,
OPENAI_IMAGE_SYNC_FINALIZE_REPORT_KIND, OPENAI_IMAGE_SYNC_SUCCESS_REPORT_KIND,
OPENAI_RESPONSES_COMPACT_STREAM_SUCCESS_REPORT_KIND,
OPENAI_RESPONSES_COMPACT_SYNC_ERROR_REPORT_KIND,
OPENAI_RESPONSES_COMPACT_SYNC_FINALIZE_REPORT_KIND,
OPENAI_RESPONSES_COMPACT_SYNC_SUCCESS_REPORT_KIND, OPENAI_RESPONSES_STREAM_SUCCESS_REPORT_KIND,
OPENAI_RESPONSES_SYNC_ERROR_REPORT_KIND, OPENAI_RESPONSES_SYNC_FINALIZE_REPORT_KIND,
OPENAI_RESPONSES_SYNC_SUCCESS_REPORT_KIND, OPENAI_SEARCH_SYNC_SUCCESS_REPORT_KIND,
OPENAI_VIDEO_CREATE_SYNC_FINALIZE_REPORT_KIND,
};
@@ -0,0 +1,48 @@
pub const GEMINI_FILES_GET_PLAN_KIND: &str = "gemini_files_get";
pub const GEMINI_FILES_UPLOAD_PLAN_KIND: &str = "gemini_files_upload";
pub const GEMINI_FILES_LIST_PLAN_KIND: &str = "gemini_files_list";
pub const GEMINI_FILES_DELETE_PLAN_KIND: &str = "gemini_files_delete";
pub const GEMINI_FILES_DOWNLOAD_PLAN_KIND: &str = "gemini_files_download";
pub const OPENAI_IMAGE_STREAM_PLAN_KIND: &str = "openai_image_stream";
pub const OPENAI_IMAGE_SYNC_PLAN_KIND: &str = "openai_image_sync";
pub const OPENAI_VIDEO_CONTENT_PLAN_KIND: &str = "openai_video_content";
pub const OPENAI_VIDEO_CANCEL_SYNC_PLAN_KIND: &str = "openai_video_cancel_sync";
pub const OPENAI_VIDEO_REMIX_SYNC_PLAN_KIND: &str = "openai_video_remix_sync";
pub const OPENAI_VIDEO_DELETE_SYNC_PLAN_KIND: &str = "openai_video_delete_sync";
pub const GEMINI_VIDEO_CREATE_SYNC_PLAN_KIND: &str = "gemini_video_create_sync";
pub const GEMINI_VIDEO_CANCEL_SYNC_PLAN_KIND: &str = "gemini_video_cancel_sync";
pub const OPENAI_CHAT_STREAM_PLAN_KIND: &str = "openai_chat_stream";
pub const CLAUDE_CHAT_STREAM_PLAN_KIND: &str = "claude_chat_stream";
pub const GEMINI_CHAT_STREAM_PLAN_KIND: &str = "gemini_chat_stream";
pub const GEMINI_INTERACTIONS_STREAM_PLAN_KIND: &str = "gemini_interactions_stream";
pub const OPENAI_RESPONSES_STREAM_PLAN_KIND: &str = "openai_responses_stream";
pub const OPENAI_RESPONSES_COMPACT_STREAM_PLAN_KIND: &str = "openai_responses_compact_stream";
pub const CLAUDE_CLI_STREAM_PLAN_KIND: &str = "claude_cli_stream";
pub const GEMINI_CLI_STREAM_PLAN_KIND: &str = "gemini_cli_stream";
pub const OPENAI_VIDEO_CREATE_SYNC_PLAN_KIND: &str = "openai_video_create_sync";
pub const OPENAI_CHAT_SYNC_PLAN_KIND: &str = "openai_chat_sync";
pub const OPENAI_EMBEDDING_SYNC_PLAN_KIND: &str = "openai_embedding_sync";
pub const OPENAI_RERANK_SYNC_PLAN_KIND: &str = "openai_rerank_sync";
pub const OPENAI_SEARCH_SYNC_PLAN_KIND: &str = "openai_search_sync";
pub const GEMINI_EMBEDDING_SYNC_PLAN_KIND: &str = "gemini_embedding_sync";
pub const OPENAI_RESPONSES_SYNC_PLAN_KIND: &str = "openai_responses_sync";
pub const OPENAI_RESPONSES_COMPACT_SYNC_PLAN_KIND: &str = "openai_responses_compact_sync";
pub const CLAUDE_CHAT_SYNC_PLAN_KIND: &str = "claude_chat_sync";
pub const GEMINI_CHAT_SYNC_PLAN_KIND: &str = "gemini_chat_sync";
pub const GEMINI_INTERACTIONS_SYNC_PLAN_KIND: &str = "gemini_interactions_sync";
pub const CLAUDE_CLI_SYNC_PLAN_KIND: &str = "claude_cli_sync";
pub const GEMINI_CLI_SYNC_PLAN_KIND: &str = "gemini_cli_sync";
pub fn is_openai_responses_stream_plan_kind(plan_kind: &str) -> bool {
matches!(
plan_kind,
OPENAI_RESPONSES_STREAM_PLAN_KIND | OPENAI_RESPONSES_COMPACT_STREAM_PLAN_KIND
)
}
pub fn is_openai_responses_sync_plan_kind(plan_kind: &str) -> bool {
matches!(
plan_kind,
OPENAI_RESPONSES_SYNC_PLAN_KIND | OPENAI_RESPONSES_COMPACT_SYNC_PLAN_KIND
)
}
@@ -0,0 +1,159 @@
use crate::contracts::{
CLAUDE_CHAT_SYNC_PLAN_KIND, CLAUDE_CLI_SYNC_PLAN_KIND, GEMINI_CHAT_SYNC_PLAN_KIND,
GEMINI_CLI_SYNC_PLAN_KIND, GEMINI_INTERACTIONS_SYNC_PLAN_KIND, OPENAI_CHAT_SYNC_PLAN_KIND,
OPENAI_EMBEDDING_SYNC_PLAN_KIND, OPENAI_IMAGE_STREAM_PLAN_KIND, OPENAI_IMAGE_SYNC_PLAN_KIND,
OPENAI_RESPONSES_COMPACT_SYNC_PLAN_KIND, OPENAI_RESPONSES_SYNC_PLAN_KIND,
};
pub const OPENAI_CHAT_SYNC_FINALIZE_REPORT_KIND: &str = "openai_chat_sync_finalize";
pub const CLAUDE_CHAT_SYNC_FINALIZE_REPORT_KIND: &str = "claude_chat_sync_finalize";
pub const GEMINI_CHAT_SYNC_FINALIZE_REPORT_KIND: &str = "gemini_chat_sync_finalize";
pub const GEMINI_INTERACTIONS_SYNC_FINALIZE_REPORT_KIND: &str = "gemini_interactions_sync_finalize";
pub const OPENAI_RESPONSES_SYNC_FINALIZE_REPORT_KIND: &str = "openai_responses_sync_finalize";
pub const OPENAI_RESPONSES_COMPACT_SYNC_FINALIZE_REPORT_KIND: &str =
"openai_responses_compact_sync_finalize";
pub const OPENAI_EMBEDDING_SYNC_FINALIZE_REPORT_KIND: &str = "openai_embedding_sync_finalize";
pub const OPENAI_IMAGE_SYNC_FINALIZE_REPORT_KIND: &str = "openai_image_sync_finalize";
pub const CLAUDE_CLI_SYNC_FINALIZE_REPORT_KIND: &str = "claude_cli_sync_finalize";
pub const GEMINI_CLI_SYNC_FINALIZE_REPORT_KIND: &str = "gemini_cli_sync_finalize";
pub const OPENAI_VIDEO_CREATE_SYNC_FINALIZE_REPORT_KIND: &str = "openai_video_create_sync_finalize";
pub const GEMINI_VIDEO_CREATE_SYNC_FINALIZE_REPORT_KIND: &str = "gemini_video_create_sync_finalize";
const LEGACY_OPENAI_CLI_SYNC_FINALIZE_REPORT_KIND: &str = "openai_cli_sync_finalize";
const LEGACY_OPENAI_COMPACT_SYNC_FINALIZE_REPORT_KIND: &str = "openai_compact_sync_finalize";
pub const OPENAI_CHAT_SYNC_SUCCESS_REPORT_KIND: &str = "openai_chat_sync_success";
pub const CLAUDE_CHAT_SYNC_SUCCESS_REPORT_KIND: &str = "claude_chat_sync_success";
pub const GEMINI_CHAT_SYNC_SUCCESS_REPORT_KIND: &str = "gemini_chat_sync_success";
pub const GEMINI_INTERACTIONS_SYNC_SUCCESS_REPORT_KIND: &str = "gemini_interactions_sync_success";
pub const OPENAI_RESPONSES_SYNC_SUCCESS_REPORT_KIND: &str = "openai_responses_sync_success";
pub const OPENAI_RESPONSES_COMPACT_SYNC_SUCCESS_REPORT_KIND: &str =
"openai_responses_compact_sync_success";
pub const OPENAI_EMBEDDING_SYNC_SUCCESS_REPORT_KIND: &str = "openai_embedding_sync_success";
pub const OPENAI_SEARCH_SYNC_SUCCESS_REPORT_KIND: &str = "openai_search_sync_success";
pub const GEMINI_EMBEDDING_SYNC_SUCCESS_REPORT_KIND: &str = "gemini_embedding_sync_success";
pub const OPENAI_IMAGE_SYNC_SUCCESS_REPORT_KIND: &str = "openai_image_sync_success";
pub const CLAUDE_CLI_SYNC_SUCCESS_REPORT_KIND: &str = "claude_cli_sync_success";
pub const GEMINI_CLI_SYNC_SUCCESS_REPORT_KIND: &str = "gemini_cli_sync_success";
pub const OPENAI_CHAT_STREAM_SUCCESS_REPORT_KIND: &str = "openai_chat_stream_success";
pub const CLAUDE_CHAT_STREAM_SUCCESS_REPORT_KIND: &str = "claude_chat_stream_success";
pub const GEMINI_CHAT_STREAM_SUCCESS_REPORT_KIND: &str = "gemini_chat_stream_success";
pub const GEMINI_INTERACTIONS_STREAM_SUCCESS_REPORT_KIND: &str =
"gemini_interactions_stream_success";
pub const OPENAI_RESPONSES_STREAM_SUCCESS_REPORT_KIND: &str = "openai_responses_stream_success";
pub const OPENAI_RESPONSES_COMPACT_STREAM_SUCCESS_REPORT_KIND: &str =
"openai_responses_compact_stream_success";
pub const OPENAI_IMAGE_STREAM_SUCCESS_REPORT_KIND: &str = "openai_image_stream_success";
pub const CLAUDE_CLI_STREAM_SUCCESS_REPORT_KIND: &str = "claude_cli_stream_success";
pub const GEMINI_CLI_STREAM_SUCCESS_REPORT_KIND: &str = "gemini_cli_stream_success";
pub const OPENAI_CHAT_SYNC_ERROR_REPORT_KIND: &str = "openai_chat_sync_error";
pub const CLAUDE_CHAT_SYNC_ERROR_REPORT_KIND: &str = "claude_chat_sync_error";
pub const GEMINI_CHAT_SYNC_ERROR_REPORT_KIND: &str = "gemini_chat_sync_error";
pub const GEMINI_INTERACTIONS_SYNC_ERROR_REPORT_KIND: &str = "gemini_interactions_sync_error";
pub const OPENAI_RESPONSES_SYNC_ERROR_REPORT_KIND: &str = "openai_responses_sync_error";
pub const OPENAI_RESPONSES_COMPACT_SYNC_ERROR_REPORT_KIND: &str =
"openai_responses_compact_sync_error";
pub const OPENAI_EMBEDDING_SYNC_ERROR_REPORT_KIND: &str = "openai_embedding_sync_error";
pub const OPENAI_IMAGE_SYNC_ERROR_REPORT_KIND: &str = "openai_image_sync_error";
pub const CLAUDE_CLI_SYNC_ERROR_REPORT_KIND: &str = "claude_cli_sync_error";
pub const GEMINI_CLI_SYNC_ERROR_REPORT_KIND: &str = "gemini_cli_sync_error";
pub fn implicit_sync_finalize_report_kind(plan_kind: &str) -> Option<&'static str> {
match plan_kind {
OPENAI_CHAT_SYNC_PLAN_KIND => Some(OPENAI_CHAT_SYNC_FINALIZE_REPORT_KIND),
CLAUDE_CHAT_SYNC_PLAN_KIND => Some(CLAUDE_CHAT_SYNC_FINALIZE_REPORT_KIND),
GEMINI_CHAT_SYNC_PLAN_KIND => Some(GEMINI_CHAT_SYNC_FINALIZE_REPORT_KIND),
GEMINI_INTERACTIONS_SYNC_PLAN_KIND => Some(GEMINI_INTERACTIONS_SYNC_FINALIZE_REPORT_KIND),
OPENAI_RESPONSES_SYNC_PLAN_KIND => Some(OPENAI_RESPONSES_SYNC_FINALIZE_REPORT_KIND),
OPENAI_RESPONSES_COMPACT_SYNC_PLAN_KIND => {
Some(OPENAI_RESPONSES_COMPACT_SYNC_FINALIZE_REPORT_KIND)
}
OPENAI_EMBEDDING_SYNC_PLAN_KIND => Some(OPENAI_EMBEDDING_SYNC_FINALIZE_REPORT_KIND),
OPENAI_IMAGE_SYNC_PLAN_KIND => Some(OPENAI_IMAGE_SYNC_FINALIZE_REPORT_KIND),
CLAUDE_CLI_SYNC_PLAN_KIND => Some(CLAUDE_CLI_SYNC_FINALIZE_REPORT_KIND),
GEMINI_CLI_SYNC_PLAN_KIND => Some(GEMINI_CLI_SYNC_FINALIZE_REPORT_KIND),
_ => None,
}
}
pub fn core_error_default_client_api_format(report_kind: &str) -> Option<&'static str> {
match report_kind {
OPENAI_CHAT_SYNC_FINALIZE_REPORT_KIND => Some("openai:chat"),
CLAUDE_CHAT_SYNC_FINALIZE_REPORT_KIND => Some("claude:messages"),
GEMINI_CHAT_SYNC_FINALIZE_REPORT_KIND => Some("gemini:generate_content"),
GEMINI_INTERACTIONS_SYNC_FINALIZE_REPORT_KIND => Some("gemini:interactions"),
OPENAI_RESPONSES_SYNC_FINALIZE_REPORT_KIND => Some("openai:responses"),
OPENAI_RESPONSES_COMPACT_SYNC_FINALIZE_REPORT_KIND => Some("openai:responses:compact"),
OPENAI_EMBEDDING_SYNC_FINALIZE_REPORT_KIND => Some("openai:embedding"),
LEGACY_OPENAI_CLI_SYNC_FINALIZE_REPORT_KIND => Some("openai:responses"),
LEGACY_OPENAI_COMPACT_SYNC_FINALIZE_REPORT_KIND => Some("openai:responses:compact"),
OPENAI_IMAGE_SYNC_FINALIZE_REPORT_KIND => Some("openai:image"),
CLAUDE_CLI_SYNC_FINALIZE_REPORT_KIND => Some("claude:messages"),
GEMINI_CLI_SYNC_FINALIZE_REPORT_KIND => Some("gemini:generate_content"),
_ => None,
}
}
pub fn core_error_background_report_kind(report_kind: &str) -> Option<&'static str> {
match report_kind {
OPENAI_CHAT_SYNC_FINALIZE_REPORT_KIND => Some(OPENAI_CHAT_SYNC_ERROR_REPORT_KIND),
CLAUDE_CHAT_SYNC_FINALIZE_REPORT_KIND => Some(CLAUDE_CHAT_SYNC_ERROR_REPORT_KIND),
GEMINI_CHAT_SYNC_FINALIZE_REPORT_KIND => Some(GEMINI_CHAT_SYNC_ERROR_REPORT_KIND),
GEMINI_INTERACTIONS_SYNC_FINALIZE_REPORT_KIND => {
Some(GEMINI_INTERACTIONS_SYNC_ERROR_REPORT_KIND)
}
OPENAI_RESPONSES_SYNC_FINALIZE_REPORT_KIND => Some(OPENAI_RESPONSES_SYNC_ERROR_REPORT_KIND),
OPENAI_RESPONSES_COMPACT_SYNC_FINALIZE_REPORT_KIND => {
Some(OPENAI_RESPONSES_COMPACT_SYNC_ERROR_REPORT_KIND)
}
OPENAI_EMBEDDING_SYNC_FINALIZE_REPORT_KIND => Some(OPENAI_EMBEDDING_SYNC_ERROR_REPORT_KIND),
LEGACY_OPENAI_CLI_SYNC_FINALIZE_REPORT_KIND => {
Some(OPENAI_RESPONSES_SYNC_ERROR_REPORT_KIND)
}
LEGACY_OPENAI_COMPACT_SYNC_FINALIZE_REPORT_KIND => {
Some(OPENAI_RESPONSES_COMPACT_SYNC_ERROR_REPORT_KIND)
}
OPENAI_IMAGE_SYNC_FINALIZE_REPORT_KIND => Some(OPENAI_IMAGE_SYNC_ERROR_REPORT_KIND),
CLAUDE_CLI_SYNC_FINALIZE_REPORT_KIND => Some(CLAUDE_CLI_SYNC_ERROR_REPORT_KIND),
GEMINI_CLI_SYNC_FINALIZE_REPORT_KIND => Some(GEMINI_CLI_SYNC_ERROR_REPORT_KIND),
_ => None,
}
}
pub fn core_success_background_report_kind(report_kind: &str) -> Option<&'static str> {
match report_kind {
OPENAI_CHAT_SYNC_FINALIZE_REPORT_KIND => Some(OPENAI_CHAT_SYNC_SUCCESS_REPORT_KIND),
CLAUDE_CHAT_SYNC_FINALIZE_REPORT_KIND => Some(CLAUDE_CHAT_SYNC_SUCCESS_REPORT_KIND),
GEMINI_CHAT_SYNC_FINALIZE_REPORT_KIND => Some(GEMINI_CHAT_SYNC_SUCCESS_REPORT_KIND),
GEMINI_INTERACTIONS_SYNC_FINALIZE_REPORT_KIND => {
Some(GEMINI_INTERACTIONS_SYNC_SUCCESS_REPORT_KIND)
}
OPENAI_IMAGE_SYNC_FINALIZE_REPORT_KIND => Some(OPENAI_IMAGE_SYNC_SUCCESS_REPORT_KIND),
OPENAI_RESPONSES_SYNC_FINALIZE_REPORT_KIND => {
Some(OPENAI_RESPONSES_SYNC_SUCCESS_REPORT_KIND)
}
OPENAI_RESPONSES_COMPACT_SYNC_FINALIZE_REPORT_KIND => {
Some(OPENAI_RESPONSES_COMPACT_SYNC_SUCCESS_REPORT_KIND)
}
OPENAI_EMBEDDING_SYNC_FINALIZE_REPORT_KIND => {
Some(OPENAI_EMBEDDING_SYNC_SUCCESS_REPORT_KIND)
}
LEGACY_OPENAI_CLI_SYNC_FINALIZE_REPORT_KIND => {
Some(OPENAI_RESPONSES_SYNC_SUCCESS_REPORT_KIND)
}
LEGACY_OPENAI_COMPACT_SYNC_FINALIZE_REPORT_KIND => {
Some(OPENAI_RESPONSES_COMPACT_SYNC_SUCCESS_REPORT_KIND)
}
CLAUDE_CLI_SYNC_FINALIZE_REPORT_KIND => Some(CLAUDE_CLI_SYNC_SUCCESS_REPORT_KIND),
GEMINI_CLI_SYNC_FINALIZE_REPORT_KIND => Some(GEMINI_CLI_SYNC_SUCCESS_REPORT_KIND),
_ => None,
}
}
pub fn implicit_stream_success_report_kind(plan_kind: &str) -> Option<&'static str> {
match plan_kind {
OPENAI_IMAGE_STREAM_PLAN_KIND => Some(OPENAI_IMAGE_STREAM_SUCCESS_REPORT_KIND),
_ => None,
}
}
@@ -0,0 +1,2 @@
pub mod request;
pub mod response;
@@ -0,0 +1,364 @@
use serde_json::{Map, Value};
use crate::formats::context::FormatContext;
use crate::formats::openai::embedding::request::{mapped_embedding_model, namespace_extensions};
use crate::protocol::canonical::{
CanonicalEmbeddingContent, CanonicalEmbeddingInput, CanonicalRequest,
};
pub fn from(body: &Value, _ctx: &FormatContext) -> Option<CanonicalRequest> {
from_raw(body)
}
pub fn from_raw(body_json: &Value) -> Option<CanonicalRequest> {
let request = body_json.as_object()?;
let model = request
.get("model")
.and_then(Value::as_str)
.map(str::trim)
.filter(|value| !value.is_empty())?
.to_string();
let contents = request
.get("input")
.and_then(Value::as_object)
.and_then(|input| input.get("contents"))
.and_then(Value::as_array)?;
let input = contents_to_embedding_input(contents)?;
let mut parameters = request
.get("parameters")
.and_then(Value::as_object)
.cloned()
.unwrap_or_default();
let dimensions = parameters
.remove("dimension")
.or_else(|| request.get("dimensions").cloned())
.and_then(|value| value.as_u64());
let parameters = (!parameters.is_empty()).then_some(parameters);
Some(CanonicalRequest {
model,
embedding: Some(crate::protocol::canonical::CanonicalEmbeddingRequest {
input,
encoding_format: None,
dimensions,
task: None,
user: None,
parameters,
extensions: namespace_extensions(
"aliyun",
request,
&["model", "input", "parameters", "dimensions"],
),
}),
..CanonicalRequest::default()
})
}
pub fn to(request: &CanonicalRequest, ctx: &FormatContext) -> Option<Value> {
let embedding = request.embedding.as_ref()?;
let contents = embedding_input_to_contents(&embedding.input)?;
if contents.is_empty() {
return None;
}
let mut output = Map::new();
output.insert(
"model".to_string(),
Value::String(mapped_embedding_model(
request,
ctx.mapped_model_or(request.model.as_str()),
)),
);
output.insert(
"input".to_string(),
Value::Object(Map::from_iter([(
"contents".to_string(),
Value::Array(contents),
)])),
);
let mut parameters = embedding.parameters.clone().unwrap_or_default();
if let Some(dimensions) = embedding.dimensions {
parameters
.entry("dimension".to_string())
.or_insert_with(|| Value::from(dimensions));
}
if !parameters.is_empty() {
output.insert("parameters".to_string(), Value::Object(parameters));
}
Some(Value::Object(output))
}
fn contents_to_embedding_input(contents: &[Value]) -> Option<CanonicalEmbeddingInput> {
if contents.is_empty() {
return None;
}
let parsed = contents
.iter()
.map(embedding_content_from_value)
.collect::<Option<Vec<_>>>()?;
if parsed.iter().all(|content| {
content.image.is_none() && content.video.is_none() && content.multi_images.is_none()
}) {
return Some(CanonicalEmbeddingInput::StringArray(
parsed
.into_iter()
.map(|content| content.text)
.collect::<Option<Vec<_>>>()?,
));
}
Some(CanonicalEmbeddingInput::Multimodal(parsed))
}
fn embedding_content_from_value(value: &Value) -> Option<CanonicalEmbeddingContent> {
let object = value.as_object()?;
let content = CanonicalEmbeddingContent {
text: object
.get("text")
.and_then(Value::as_str)
.map(str::trim)
.filter(|value| !value.is_empty())
.map(ToOwned::to_owned),
image: object
.get("image")
.and_then(Value::as_str)
.map(str::trim)
.filter(|value| !value.is_empty())
.map(ToOwned::to_owned),
video: object
.get("video")
.and_then(Value::as_str)
.map(str::trim)
.filter(|value| !value.is_empty())
.map(ToOwned::to_owned),
multi_images: match object.get("multi_images").and_then(Value::as_array) {
Some(values) => Some(
values
.iter()
.map(|value| {
value
.as_str()
.map(str::trim)
.filter(|value| !value.is_empty())
.map(ToOwned::to_owned)
})
.collect::<Option<Vec<_>>>()?,
),
None => None,
},
};
(!content.is_empty()).then_some(content)
}
fn embedding_input_to_contents(input: &CanonicalEmbeddingInput) -> Option<Vec<Value>> {
match input {
CanonicalEmbeddingInput::String(text) => {
non_empty_text_content(text).map(|content| vec![content])
}
CanonicalEmbeddingInput::StringArray(items) => items
.iter()
.map(|text| non_empty_text_content(text))
.collect(),
CanonicalEmbeddingInput::Multimodal(items) => {
items.iter().map(multimodal_content_to_value).collect()
}
CanonicalEmbeddingInput::TokenArray(_) | CanonicalEmbeddingInput::TokenArrayArray(_) => {
None
}
}
}
fn non_empty_text_content(text: &str) -> Option<Value> {
let text = text.trim();
if text.is_empty() {
None
} else {
Some(Value::Object(Map::from_iter([(
"text".to_string(),
Value::String(text.to_string()),
)])))
}
}
fn multimodal_content_to_value(content: &CanonicalEmbeddingContent) -> Option<Value> {
if content.is_empty() {
return None;
}
let mut object = Map::new();
if let Some(text) = content
.text
.as_deref()
.map(str::trim)
.filter(|value| !value.is_empty())
{
object.insert("text".to_string(), Value::String(text.to_string()));
}
if let Some(image) = content
.image
.as_deref()
.map(str::trim)
.filter(|value| !value.is_empty())
{
object.insert("image".to_string(), Value::String(image.to_string()));
}
if let Some(video) = content
.video
.as_deref()
.map(str::trim)
.filter(|value| !value.is_empty())
{
object.insert("video".to_string(), Value::String(video.to_string()));
}
if let Some(multi_images) = content
.multi_images
.as_ref()
.filter(|values| !values.is_empty() && values.iter().all(|value| !value.trim().is_empty()))
{
object.insert(
"multi_images".to_string(),
Value::Array(
multi_images
.iter()
.map(|value| Value::String(value.trim().to_string()))
.collect(),
),
);
}
if object.is_empty() {
None
} else {
Some(Value::Object(object))
}
}
#[cfg(test)]
mod tests {
use std::collections::BTreeMap;
use serde_json::{json, Map, Value};
use super::to;
use crate::formats::context::FormatContext;
use crate::protocol::canonical::{
CanonicalEmbeddingContent, CanonicalEmbeddingInput, CanonicalEmbeddingRequest,
CanonicalRequest,
};
fn canonical_embedding(input: CanonicalEmbeddingInput) -> CanonicalRequest {
CanonicalRequest {
model: "text-embedding-3-small".to_string(),
embedding: Some(CanonicalEmbeddingRequest {
input,
encoding_format: None,
dimensions: None,
task: None,
user: None,
parameters: None,
extensions: BTreeMap::new(),
}),
..CanonicalRequest::default()
}
}
#[test]
fn text_input_uses_dashscope_contents() {
let request = canonical_embedding(CanonicalEmbeddingInput::StringArray(vec![
"alpha".to_string(),
"beta".to_string(),
]));
let body = to(
&request,
&FormatContext::default().with_mapped_model("qwen3-vl-embedding"),
)
.expect("aliyun request");
assert_eq!(body["model"], "qwen3-vl-embedding");
assert_eq!(
body["input"]["contents"],
json!([{ "text": "alpha" }, { "text": "beta" }])
);
}
#[test]
fn multimodal_input_and_parameters_use_dashscope_contract() {
let mut request = canonical_embedding(CanonicalEmbeddingInput::Multimodal(vec![
CanonicalEmbeddingContent {
text: Some("white running shoes".to_string()),
image: None,
video: None,
multi_images: None,
},
CanonicalEmbeddingContent {
text: None,
image: Some("https://example.com/shoe.png".to_string()),
video: None,
multi_images: None,
},
CanonicalEmbeddingContent {
text: None,
image: None,
video: None,
multi_images: Some(vec![
"https://example.com/a.png".to_string(),
"https://example.com/b.png".to_string(),
]),
},
]));
let embedding = request.embedding.as_mut().expect("embedding request");
embedding.dimensions = Some(1024);
embedding.parameters = Some(Map::from_iter([
("enable_fusion".to_string(), Value::Bool(true)),
("res_level".to_string(), Value::from(2_u64)),
("max_video_frames".to_string(), Value::from(64_u64)),
]));
let body = to(
&request,
&FormatContext::default().with_mapped_model("qwen3-vl-embedding"),
)
.expect("aliyun request");
assert_eq!(
body["input"]["contents"],
json!([
{ "text": "white running shoes" },
{ "image": "https://example.com/shoe.png" },
{ "multi_images": ["https://example.com/a.png", "https://example.com/b.png"] }
])
);
assert_eq!(body["parameters"]["dimension"], 1024);
assert_eq!(body["parameters"]["enable_fusion"], true);
assert_eq!(body["parameters"]["res_level"], 2);
assert_eq!(body["parameters"]["max_video_frames"], 64);
}
#[test]
fn parameter_dimension_wins_over_openai_dimensions() {
let mut request = canonical_embedding(CanonicalEmbeddingInput::String("alpha".to_string()));
let embedding = request.embedding.as_mut().expect("embedding request");
embedding.dimensions = Some(1024);
embedding.parameters = Some(Map::from_iter([(
"dimension".to_string(),
Value::from(512_u64),
)]));
let body = to(
&request,
&FormatContext::default().with_mapped_model("qwen3-vl-embedding"),
)
.expect("aliyun request");
assert_eq!(body["parameters"]["dimension"], 512);
}
#[test]
fn token_arrays_are_not_convertible() {
let request = canonical_embedding(CanonicalEmbeddingInput::TokenArray(vec![1, 2, 3]));
assert!(to(
&request,
&FormatContext::default().with_mapped_model("qwen3-vl-embedding"),
)
.is_none());
}
}
@@ -0,0 +1,134 @@
use std::collections::BTreeMap;
use serde_json::{Map, Value};
use crate::formats::openai::embedding::request::namespace_extensions;
use crate::protocol::canonical::{CanonicalEmbedding, CanonicalEmbeddingResponse, CanonicalUsage};
pub fn from(body_json: &Value) -> Option<CanonicalEmbeddingResponse> {
let body = body_json.as_object()?;
if body.contains_key("error") || body.contains_key("code") && body.contains_key("message") {
return None;
}
let data = body
.get("output")?
.as_object()?
.get("embeddings")?
.as_array()?;
let mut embeddings = Vec::new();
for (fallback_index, item) in data.iter().enumerate() {
let item_object = item.as_object()?;
let values = item_object.get("embedding")?.as_array()?;
let embedding = values
.iter()
.map(Value::as_f64)
.collect::<Option<Vec<_>>>()?;
let mut extensions =
namespace_extensions("aliyun", item_object, &["index", "embedding", "type"]);
if let Some(value) = item_object.get("type").cloned() {
extensions.insert(
"openai".to_string(),
Value::Object(Map::from_iter([("type".to_string(), value)])),
);
}
embeddings.push(CanonicalEmbedding {
index: item_object
.get("index")
.and_then(Value::as_u64)
.and_then(|value| usize::try_from(value).ok())
.unwrap_or(fallback_index),
embedding,
extensions,
});
}
let request_id = body.get("request_id").and_then(Value::as_str);
let mut extensions =
namespace_extensions("aliyun", body, &["output", "usage", "request_id", "model"]);
if let Some(request_id) = request_id {
extensions.insert(
"openai".to_string(),
Value::Object(Map::from_iter([(
"request_id".to_string(),
Value::String(request_id.to_string()),
)])),
);
}
Some(CanonicalEmbeddingResponse {
id: request_id.unwrap_or("aliyun-request-unknown").to_string(),
model: body
.get("model")
.and_then(Value::as_str)
.unwrap_or("unknown")
.to_string(),
embeddings,
usage: aliyun_usage_to_canonical(body.get("usage")),
extensions,
})
}
fn aliyun_usage_to_canonical(value: Option<&Value>) -> Option<CanonicalUsage> {
let usage = value?.as_object()?;
let input_tokens = usage
.get("input_tokens")
.and_then(Value::as_u64)
.unwrap_or(0);
let output_tokens = usage
.get("output_tokens")
.and_then(Value::as_u64)
.unwrap_or(0);
Some(CanonicalUsage {
input_tokens,
output_tokens,
total_tokens: usage
.get("total_tokens")
.and_then(Value::as_u64)
.unwrap_or(input_tokens.saturating_add(output_tokens)),
extensions: BTreeMap::from([("aliyun".to_string(), Value::Object(usage.clone()))]),
..CanonicalUsage::default()
})
}
#[cfg(test)]
mod tests {
use serde_json::json;
use super::from;
use crate::formats::openai::embedding::response::to as to_openai;
#[test]
fn parses_dashscope_embeddings_to_openai_compatible_shape() {
let body = json!({
"output": {
"embeddings": [
{
"index": 0,
"embedding": [0.1, 0.2, 0.3],
"type": "fused"
}
]
},
"usage": {
"input_tokens": 432,
"input_tokens_details": {
"image_tokens": 402,
"text_tokens": 30
},
"output_tokens": 1,
"total_tokens": 433
},
"request_id": "aliyun-request-1"
});
let canonical = from(&body).expect("aliyun response");
let emitted = to_openai(&canonical).expect("openai response");
assert_eq!(emitted["request_id"], "aliyun-request-1");
assert_eq!(emitted["data"][0]["embedding"], json!([0.1, 0.2, 0.3]));
assert_eq!(emitted["data"][0]["type"], "fused");
assert_eq!(emitted["usage"]["prompt_tokens"], 432);
assert_eq!(emitted["usage"]["completion_tokens"], 1);
assert_eq!(emitted["usage"]["total_tokens"], 433);
}
}
@@ -0,0 +1 @@
pub mod embedding;
@@ -0,0 +1,53 @@
use crate::contracts::{CLAUDE_CHAT_STREAM_PLAN_KIND, CLAUDE_CHAT_SYNC_PLAN_KIND};
use crate::formats::shared::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);
}
}
@@ -0,0 +1,53 @@
use crate::contracts::{CLAUDE_CLI_STREAM_PLAN_KIND, CLAUDE_CLI_SYNC_PLAN_KIND};
use crate::formats::shared::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);
}
}
@@ -0,0 +1,16 @@
pub mod chat_spec;
pub mod cli_spec;
pub mod request;
pub mod response;
pub mod spec;
pub mod stream;
use crate::formats::shared::family::LocalStandardSpec;
pub fn resolve_sync_spec(plan_kind: &str) -> Option<LocalStandardSpec> {
chat_spec::resolve_sync_spec(plan_kind).or_else(|| cli_spec::resolve_sync_spec(plan_kind))
}
pub fn resolve_stream_spec(plan_kind: &str) -> Option<LocalStandardSpec> {
chat_spec::resolve_stream_spec(plan_kind).or_else(|| cli_spec::resolve_stream_spec(plan_kind))
}
@@ -0,0 +1,192 @@
use serde_json::{json, Map, Value};
use crate::{
formats::{
context::FormatContext,
openai::shared::{
map_openai_reasoning_effort_to_claude_output,
map_openai_reasoning_effort_to_thinking_budget,
},
shared::model_directives::claude_model_uses_adaptive_effort,
},
protocol::canonical::{
canonical_extension_object_mut, canonical_instructions_to_claude_system,
canonical_messages_to_claude, canonical_openai_reasoning_effort,
canonical_tool_choice_to_claude, canonical_tools_to_claude, claude_extensions,
claude_generation_config, claude_messages_to_canonical, claude_parallel_tool_calls,
claude_system_to_canonical_instructions, claude_thinking_to_canonical,
claude_tool_choice_to_canonical, claude_tools_to_canonical,
compact_canonical_claude_messages, insert_f64, mark_claude_messages_request_source,
namespace_extension_object, CanonicalRequest,
},
};
pub fn from(body: &Value, _ctx: &FormatContext) -> Option<CanonicalRequest> {
from_raw(body)
}
pub fn to(request: &CanonicalRequest, ctx: &FormatContext) -> Option<Value> {
to_raw(
request,
ctx.mapped_model_or(request.model.as_str()),
ctx.upstream_is_stream,
)
}
pub fn from_raw(body_json: &Value) -> Option<CanonicalRequest> {
let request = body_json.as_object()?;
let mut canonical = CanonicalRequest {
model: request
.get("model")
.and_then(Value::as_str)
.unwrap_or_default()
.to_string(),
..CanonicalRequest::default()
};
canonical.instructions = claude_system_to_canonical_instructions(request.get("system"))?;
let system_text = canonical
.instructions
.iter()
.map(|instruction| instruction.text.as_str())
.filter(|text| !text.trim().is_empty())
.collect::<Vec<_>>()
.join("\n\n");
if !system_text.is_empty() {
canonical.system = Some(system_text);
}
canonical.messages = claude_messages_to_canonical(request.get("messages"))?;
canonical.generation = claude_generation_config(request);
let (tools, builtin_tools, web_search_options) =
claude_tools_to_canonical(request.get("tools"))?;
canonical.tools = tools;
canonical.tool_choice = claude_tool_choice_to_canonical(request.get("tool_choice"));
canonical.parallel_tool_calls = claude_parallel_tool_calls(request.get("tool_choice"));
canonical.metadata = request.get("metadata").cloned();
canonical.thinking = claude_thinking_to_canonical(request);
canonical.extensions = claude_extensions(
request,
&[
"model",
"system",
"messages",
"max_tokens",
"temperature",
"top_p",
"top_k",
"stop",
"stop_sequences",
"stream",
"tools",
"tool_choice",
"metadata",
"thinking",
"output_config",
],
);
mark_claude_messages_request_source(&mut canonical.extensions);
if !builtin_tools.is_empty() {
canonical_extension_object_mut(&mut canonical.extensions, "claude")
.insert("builtin_tools".to_string(), Value::Array(builtin_tools));
}
if let Some(web_search_options) = web_search_options {
canonical_extension_object_mut(&mut canonical.extensions, "openai")
.insert("web_search_options".to_string(), web_search_options);
}
if let Some(output_config) = request.get("output_config").cloned() {
canonical_extension_object_mut(&mut canonical.extensions, "claude")
.insert("output_config".to_string(), output_config);
}
Some(canonical)
}
pub fn to_raw(
canonical: &CanonicalRequest,
mapped_model: &str,
upstream_is_stream: bool,
) -> Option<Value> {
let mut output = Map::new();
output.insert("model".to_string(), Value::String(mapped_model.to_string()));
output.insert(
"messages".to_string(),
Value::Array(compact_canonical_claude_messages(
canonical_messages_to_claude(canonical)?,
)),
);
output.insert(
"max_tokens".to_string(),
Value::from(canonical.generation.max_tokens.unwrap_or(8192)),
);
if let Some(system) = canonical_instructions_to_claude_system(&canonical.instructions) {
output.insert("system".to_string(), system);
} else if let Some(system) = canonical
.system
.as_ref()
.filter(|value| !value.trim().is_empty())
{
output.insert("system".to_string(), Value::String(system.clone()));
}
if upstream_is_stream {
output.insert("stream".to_string(), Value::Bool(true));
}
insert_f64(&mut output, "temperature", canonical.generation.temperature);
insert_f64(&mut output, "top_p", canonical.generation.top_p);
if let Some(top_k) = canonical.generation.top_k {
output.insert("top_k".to_string(), Value::from(top_k));
}
if let Some(stop_sequences) = &canonical.generation.stop_sequences {
output.insert(
"stop_sequences".to_string(),
Value::Array(stop_sequences.iter().cloned().map(Value::String).collect()),
);
}
let tools = canonical_tools_to_claude(canonical);
if !tools.is_empty() {
output.insert("tools".to_string(), Value::Array(tools));
}
if let Some(tool_choice) = canonical_tool_choice_to_claude(
canonical.tool_choice.as_ref(),
canonical.parallel_tool_calls,
) {
output.insert("tool_choice".to_string(), tool_choice);
}
if let Some(metadata) = canonical.metadata.clone() {
output.insert("metadata".to_string(), metadata);
}
if let Some(thinking) = canonical.thinking.as_ref() {
let openai_effort = canonical_openai_reasoning_effort(thinking);
let budget_tokens = thinking
.budget_tokens
.or_else(|| openai_effort.and_then(map_openai_reasoning_effort_to_thinking_budget));
let uses_adaptive = claude_model_uses_adaptive_effort(mapped_model)
|| claude_model_uses_adaptive_effort(canonical.model.as_str());
if thinking.enabled || budget_tokens.is_some() {
let thinking_config = if uses_adaptive {
json!({"type": "adaptive"})
} else {
json!({
"type": "enabled",
"budget_tokens": budget_tokens.unwrap_or(1024),
})
};
output.insert("thinking".to_string(), thinking_config);
}
if let Some(output_effort) =
openai_effort.and_then(map_openai_reasoning_effort_to_claude_output)
{
output.insert(
"output_config".to_string(),
json!({
"effort": output_effort,
}),
);
}
}
output.extend(namespace_extension_object(
&canonical.extensions,
"claude",
&output,
));
Some(Value::Object(output))
}
@@ -0,0 +1,118 @@
use std::collections::BTreeMap;
use serde_json::{json, Value};
use crate::{
formats::context::FormatContext,
protocol::canonical::{
canonical_blocks_to_claude, canonical_extension_object_mut,
canonical_stop_reason_to_claude, canonical_usage_to_claude,
claude_content_to_canonical_blocks, claude_extensions, claude_stop_reason_to_canonical,
claude_usage_to_canonical, namespace_extension_object, CanonicalResponse,
CanonicalResponseOutput, CanonicalRole,
},
};
pub fn from(body: &Value, _ctx: &FormatContext) -> Option<CanonicalResponse> {
from_raw(body)
}
pub fn to(response: &CanonicalResponse, _ctx: &FormatContext) -> Option<Value> {
Some(to_raw(response))
}
pub fn from_raw(body_json: &Value) -> Option<CanonicalResponse> {
let body = body_json.as_object()?;
if body.contains_key("error") || body.get("type").and_then(Value::as_str) == Some("error") {
return None;
}
let content = claude_content_to_canonical_blocks(body.get("content"))?;
let stop_reason =
claude_stop_reason_to_canonical(body.get("stop_reason").and_then(Value::as_str));
let mut extensions = claude_extensions(
body,
&[
"id",
"type",
"role",
"model",
"content",
"stop_reason",
"stop_sequence",
"usage",
],
);
if let Some(raw_stop_reason) = body.get("stop_reason").cloned() {
canonical_extension_object_mut(&mut extensions, "claude")
.insert("raw_stop_reason".to_string(), raw_stop_reason);
}
if let Some(raw_stop_sequence) = body.get("stop_sequence").cloned() {
canonical_extension_object_mut(&mut extensions, "claude")
.insert("raw_stop_sequence".to_string(), raw_stop_sequence);
}
Some(CanonicalResponse {
id: body
.get("id")
.and_then(Value::as_str)
.unwrap_or("msg-unknown")
.to_string(),
model: body
.get("model")
.and_then(Value::as_str)
.unwrap_or("unknown")
.to_string(),
outputs: vec![CanonicalResponseOutput {
index: 0,
role: CanonicalRole::Assistant,
content: content.clone(),
stop_reason: stop_reason.clone(),
extensions: BTreeMap::new(),
}],
content,
stop_reason,
usage: claude_usage_to_canonical(body.get("usage")),
extensions,
})
}
pub fn to_raw(canonical: &CanonicalResponse) -> Value {
let mut content = canonical_blocks_to_claude(&canonical.content, CanonicalRole::Assistant)
.unwrap_or_default();
if content.is_empty() {
content.push(json!({
"type": "text",
"text": "",
}));
}
let mut response = json!({
"id": canonical.id,
"type": "message",
"role": "assistant",
"model": canonical.model,
"content": content,
"stop_reason": canonical_stop_reason_to_claude(canonical.stop_reason.as_ref()),
"usage": canonical.usage.as_ref().map(canonical_usage_to_claude).unwrap_or_else(|| json!({
"input_tokens": 0,
"output_tokens": 0,
})),
});
if let Some(claude) = canonical
.extensions
.get("claude")
.and_then(Value::as_object)
{
if let Some(raw_stop_reason) = claude.get("raw_stop_reason").cloned() {
response["stop_reason"] = raw_stop_reason;
}
if let Some(raw_stop_sequence) = claude.get("raw_stop_sequence").cloned() {
response["stop_sequence"] = raw_stop_sequence;
}
}
if let Some(object) = response.as_object_mut() {
let mut extra = namespace_extension_object(&canonical.extensions, "claude", object);
extra.remove("raw_stop_reason");
extra.remove("raw_stop_sequence");
object.extend(extra);
}
response
}
@@ -0,0 +1 @@
pub use super::{chat_spec, cli_spec, resolve_stream_spec, resolve_sync_spec};
File diff suppressed because it is too large Load Diff
@@ -0,0 +1 @@
pub mod messages;
@@ -0,0 +1,233 @@
use std::{error::Error, fmt};
use serde::{Deserialize, Serialize};
use serde_json::{json, Value};
#[derive(Debug, Clone, Default)]
pub struct FormatContext {
pub mapped_model: Option<String>,
pub request_path: Option<String>,
pub upstream_is_stream: bool,
pub report_context: Option<Value>,
}
impl FormatContext {
pub fn with_mapped_model(mut self, mapped_model: impl Into<String>) -> Self {
self.mapped_model = Some(mapped_model.into());
self
}
pub fn with_request_path(mut self, request_path: impl Into<String>) -> Self {
self.request_path = Some(request_path.into());
self
}
pub fn with_upstream_stream(mut self, upstream_is_stream: bool) -> Self {
self.upstream_is_stream = upstream_is_stream;
self
}
pub fn with_report_context(mut self, report_context: Value) -> Self {
self.report_context = Some(report_context);
self
}
pub fn without_runtime_request_edits(&self) -> Self {
Self {
mapped_model: None,
request_path: self.request_path.clone(),
upstream_is_stream: false,
report_context: self.report_context.clone(),
}
}
pub(crate) fn mapped_model_or<'a>(&'a self, fallback: &'a str) -> &'a str {
self.mapped_model
.as_deref()
.filter(|value| !value.trim().is_empty())
.unwrap_or(fallback)
}
pub(crate) fn report_context_value(&self) -> Value {
self.report_context.clone().unwrap_or_else(|| {
json!({
"mapped_model": self.mapped_model,
})
})
}
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "kebab-case")]
pub enum ConversionFieldStatus {
Native,
Mapped,
ExtensionPreserved,
Unaudited,
Unsupported,
InvalidEnum,
LossyBlocked,
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct ConversionFieldRecord {
pub field: String,
pub status: ConversionFieldStatus,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub detail: Option<String>,
}
impl ConversionFieldRecord {
pub fn new(
field: impl Into<String>,
status: ConversionFieldStatus,
detail: Option<String>,
) -> Self {
Self {
field: field.into(),
status,
detail,
}
}
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct ConversionReport {
pub source_format: String,
pub target_format: String,
#[serde(default, skip_serializing_if = "Vec::is_empty")]
pub fields: Vec<ConversionFieldRecord>,
}
impl ConversionReport {
pub fn new(source_format: impl Into<String>, target_format: impl Into<String>) -> Self {
Self {
source_format: source_format.into(),
target_format: target_format.into(),
fields: Vec::new(),
}
}
pub fn record(
&mut self,
field: impl Into<String>,
status: ConversionFieldStatus,
detail: Option<String>,
) {
self.fields
.push(ConversionFieldRecord::new(field, status, detail));
}
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct Converted<T> {
pub value: T,
pub report: ConversionReport,
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum FormatError {
UnsupportedFormat(String),
RequestParseFailed {
format: String,
},
RequestEmitFailed {
format: String,
},
ResponseParseFailed {
format: String,
},
ResponseEmitFailed {
format: String,
},
UnsupportedField {
format: String,
field: String,
reason: String,
},
UnauditedField {
source_format: String,
target_format: String,
field: String,
reason: String,
},
InvalidEnumValue {
format: String,
field: String,
value: String,
},
LossyConversionBlocked {
source_format: String,
target_format: String,
field: String,
reason: String,
},
InvalidTargetField {
format: String,
field: String,
reason: String,
},
}
impl fmt::Display for FormatError {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
Self::UnsupportedFormat(format) => write!(f, "unsupported AI format: {format}"),
Self::RequestParseFailed { format } => {
write!(f, "failed to parse {format} request")
}
Self::RequestEmitFailed { format } => write!(f, "failed to emit {format} request"),
Self::ResponseParseFailed { format } => {
write!(f, "failed to parse {format} response")
}
Self::ResponseEmitFailed { format } => {
write!(f, "failed to emit {format} response")
}
Self::UnsupportedField {
format,
field,
reason,
} => {
write!(f, "unsupported field {field} in {format}: {reason}")
}
Self::UnauditedField {
source_format,
target_format,
field,
reason,
} => {
write!(
f,
"unaudited field {field} in {source_format} cannot be converted to {target_format}: {reason}"
)
}
Self::InvalidEnumValue {
format,
field,
value,
} => {
write!(f, "invalid enum value {value:?} for {format}.{field}")
}
Self::LossyConversionBlocked {
source_format,
target_format,
field,
reason,
} => {
write!(
f,
"lossy conversion blocked from {source_format} to {target_format} at {field}: {reason}"
)
}
Self::InvalidTargetField {
format,
field,
reason,
} => {
write!(f, "invalid target field {field} for {format}: {reason}")
}
}
}
}
impl Error for FormatError {}
@@ -0,0 +1,2 @@
pub mod request;
pub mod response;
File diff suppressed because it is too large Load Diff
@@ -0,0 +1,325 @@
//! Pairwise response conversion helpers.
//!
//! These helpers keep the call sites readable while delegating wire-format
//! parsing and emitting to `formats::<format>::response` through the registry's
//! canonical IR path.
use serde_json::{json, Value};
use crate::formats::{
context::FormatContext,
openai::responses::response::ensure_modern_openai_responses_response_fields, registry,
};
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct OpenAiResponsesResponseUsage {
pub prompt_tokens: u64,
pub output_tokens: u64,
pub total_tokens: u64,
}
pub fn convert_claude_chat_response_to_openai_chat(
body_json: &Value,
report_context: &Value,
) -> Option<Value> {
registry::convert_response(
"claude:messages",
"openai:chat",
body_json,
&response_context(report_context),
)
.ok()
}
pub fn convert_gemini_chat_response_to_openai_chat(
body_json: &Value,
report_context: &Value,
) -> Option<Value> {
registry::convert_response(
"gemini:generate_content",
"openai:chat",
body_json,
&response_context(report_context),
)
.ok()
}
pub fn convert_openai_chat_response_to_claude_chat(
body_json: &Value,
report_context: &Value,
) -> Option<Value> {
registry::convert_response(
"openai:chat",
"claude:messages",
body_json,
&response_context(report_context),
)
.ok()
}
pub fn convert_openai_chat_response_to_gemini_chat(
body_json: &Value,
report_context: &Value,
) -> Option<Value> {
registry::convert_response(
"openai:chat",
"gemini:generate_content",
body_json,
&response_context(report_context),
)
.ok()
}
pub fn convert_openai_responses_response_to_openai_chat(
body_json: &Value,
report_context: &Value,
) -> Option<Value> {
registry::convert_response(
"openai:responses",
"openai:chat",
body_json,
&response_context(report_context),
)
.ok()
}
pub fn convert_openai_chat_response_to_openai_responses(
body_json: &Value,
report_context: &Value,
compact: bool,
) -> Option<Value> {
let target_format = if compact {
"openai:responses:compact"
} else {
"openai:responses"
};
registry::convert_response(
"openai:chat",
target_format,
body_json,
&response_context(report_context),
)
.ok()
}
pub fn convert_claude_response_to_openai_responses(
body_json: &Value,
report_context: &Value,
) -> Option<Value> {
registry::convert_response(
"claude:messages",
"openai:responses",
body_json,
&response_context(report_context),
)
.ok()
}
pub fn convert_gemini_response_to_openai_responses(
body_json: &Value,
report_context: &Value,
) -> Option<Value> {
registry::convert_response(
"gemini:generate_content",
"openai:responses",
body_json,
&response_context(report_context),
)
.ok()
}
pub fn build_openai_responses_response(
response_id: &str,
model: &str,
text: &str,
function_calls: Vec<Value>,
prompt_tokens: u64,
output_tokens: u64,
total_tokens: u64,
) -> Value {
let content = if text.is_empty() {
Vec::new()
} else {
vec![json!({
"type": "output_text",
"text": text,
"annotations": []
})]
};
build_openai_responses_response_with_content(
response_id,
model,
content,
Vec::new(),
function_calls,
OpenAiResponsesResponseUsage {
prompt_tokens,
output_tokens,
total_tokens,
},
)
}
pub fn build_openai_responses_response_with_reasoning(
response_id: &str,
model: &str,
text: &str,
reasoning_summaries: Vec<String>,
function_calls: Vec<Value>,
usage: OpenAiResponsesResponseUsage,
) -> Value {
let content = if text.is_empty() {
Vec::new()
} else {
vec![json!({
"type": "output_text",
"text": text,
"annotations": []
})]
};
build_openai_responses_response_with_content(
response_id,
model,
content,
reasoning_summaries,
function_calls,
usage,
)
}
pub fn build_openai_responses_response_with_content(
response_id: &str,
model: &str,
content: Vec<Value>,
reasoning_summaries: Vec<String>,
function_calls: Vec<Value>,
usage: OpenAiResponsesResponseUsage,
) -> Value {
let mut output = Vec::new();
for (index, summary) in reasoning_summaries.into_iter().enumerate() {
let trimmed = summary.trim();
if trimmed.is_empty() {
continue;
}
output.push(json!({
"type": "reasoning",
"id": format!("{response_id}_rs_{index}"),
"status": "completed",
"summary": [{
"type": "summary_text",
"text": trimmed,
}]
}));
}
if !content.is_empty() {
output.push(json!({
"type": "message",
"id": format!("{response_id}_msg"),
"role": "assistant",
"status": "completed",
"content": content
}));
}
output.extend(function_calls);
let mut response = json!({
"id": response_id,
"object": "response",
"status": "completed",
"model": model,
"output": output,
"usage": {
"input_tokens": usage.prompt_tokens,
"output_tokens": usage.output_tokens,
"total_tokens": usage.total_tokens,
}
});
if let Some(response_object) = response.as_object_mut() {
ensure_modern_openai_responses_response_fields(response_object);
}
response
}
fn response_context(report_context: &Value) -> FormatContext {
let mut context = FormatContext::default().with_report_context(report_context.clone());
if let Some(model) = report_context
.get("mapped_model")
.and_then(Value::as_str)
.or_else(|| report_context.get("model").and_then(Value::as_str))
.filter(|value| !value.trim().is_empty())
{
context = context.with_mapped_model(model);
}
context
}
#[cfg(test)]
mod tests {
use serde_json::json;
use super::{
convert_claude_chat_response_to_openai_chat,
convert_openai_chat_response_to_openai_responses,
};
#[test]
fn pairwise_response_helper_routes_through_registry() {
let body = json!({
"id": "chatcmpl-test",
"object": "chat.completion",
"model": "gpt-source",
"choices": [{
"index": 0,
"message": {"role": "assistant", "content": "hello"},
"finish_reason": "stop"
}],
"usage": {"prompt_tokens": 1, "completion_tokens": 2, "total_tokens": 3}
});
let converted = convert_openai_chat_response_to_openai_responses(&body, &json!({}), false)
.expect("responses response");
assert_eq!(converted["object"], "response");
assert_eq!(converted["output"][0]["type"], "message");
assert_eq!(converted["output_text"], "hello");
assert!(converted["created_at"].as_i64().is_some());
assert!(converted["completed_at"].as_i64().is_some());
}
#[test]
fn manual_responses_response_builder_emits_modern_fields() {
let response = super::build_openai_responses_response(
"resp_manual_123",
"gpt-5",
"Hello manual",
Vec::new(),
1,
2,
3,
);
assert_eq!(response["output_text"], "Hello manual");
assert!(response["created_at"].as_i64().is_some());
assert!(response["completed_at"].as_i64().is_some());
}
#[test]
fn pairwise_response_helper_uses_report_context_model_fallback() {
let body = json!({
"id": "msg-test",
"type": "message",
"role": "assistant",
"content": [{"type": "text", "text": "hello"}],
"stop_reason": "end_turn",
"usage": {"input_tokens": 1, "output_tokens": 2}
});
let converted = convert_claude_chat_response_to_openai_chat(
&body,
&json!({"mapped_model": "gpt-target"}),
)
.expect("openai chat response");
assert_eq!(converted["model"], "gpt-target");
assert_eq!(converted["choices"][0]["message"]["content"], "hello");
}
}
@@ -0,0 +1 @@
pub mod request;
@@ -0,0 +1,44 @@
use serde_json::Map;
use serde_json::Value;
use crate::formats::context::FormatContext;
use crate::formats::openai::embedding::request::mapped_embedding_model;
use crate::protocol::canonical::{namespace_extension_object, CanonicalRequest};
pub fn from(body: &Value, _ctx: &FormatContext) -> Option<CanonicalRequest> {
crate::formats::openai::embedding::request::from_namespace(body, "doubao")
}
pub fn to(request: &CanonicalRequest, ctx: &FormatContext) -> Option<Value> {
let embedding = request.embedding.as_ref()?;
let items = embedding.input.as_string_items()?;
if items.is_empty() || items.iter().any(|value| value.trim().is_empty()) {
return None;
}
let mut output = Map::new();
output.insert(
"model".to_string(),
Value::String(mapped_embedding_model(
request,
ctx.mapped_model_or(request.model.as_str()),
)),
);
output.insert(
"input".to_string(),
Value::Array(
items
.into_iter()
.map(|text| Value::String(text.to_string()))
.collect(),
),
);
if let Some(dimensions) = embedding.dimensions {
output.insert("dimensions".to_string(), Value::from(dimensions));
}
output.extend(namespace_extension_object(
&embedding.extensions,
"doubao",
&output,
));
Some(Value::Object(output))
}
@@ -0,0 +1 @@
pub mod embedding;
@@ -0,0 +1,2 @@
pub mod request;
pub mod response;
@@ -0,0 +1,310 @@
use serde_json::{json, Map, Value};
use crate::formats::context::FormatContext;
use crate::formats::openai::embedding::request::{mapped_embedding_model, namespace_extensions};
use crate::protocol::canonical::{
CanonicalEmbeddingInput, CanonicalEmbeddingRequest, CanonicalRequest,
};
pub fn from(body: &Value, _ctx: &FormatContext) -> Option<CanonicalRequest> {
from_raw(body)
}
pub fn from_raw(body_json: &Value) -> Option<CanonicalRequest> {
let request = body_json.as_object()?;
if let Some(requests) = request.get("requests").and_then(Value::as_array) {
return from_batch_requests(request, requests);
}
let item = parse_gemini_embedding_request_object(request)?;
Some(CanonicalRequest {
model: item.model,
embedding: Some(CanonicalEmbeddingRequest {
input: CanonicalEmbeddingInput::String(item.text),
encoding_format: None,
dimensions: item.dimensions,
task: item.task,
user: None,
parameters: None,
extensions: namespace_extensions(
"gemini",
request,
&[
"model",
"content",
"outputDimensionality",
"output_dimensionality",
"taskType",
"task_type",
],
),
}),
..CanonicalRequest::default()
})
}
fn from_batch_requests(
request: &Map<String, Value>,
requests: &[Value],
) -> Option<CanonicalRequest> {
if requests.is_empty() {
return None;
}
let items = requests
.iter()
.map(|request| parse_gemini_embedding_request_object(request.as_object()?))
.collect::<Option<Vec<_>>>()?;
let first = items.first()?;
if items.iter().any(|item| {
item.model != first.model || item.dimensions != first.dimensions || item.task != first.task
}) {
return None;
}
let model = first.model.clone();
let dimensions = first.dimensions;
let task = first.task.clone();
Some(CanonicalRequest {
model,
embedding: Some(CanonicalEmbeddingRequest {
input: CanonicalEmbeddingInput::StringArray(
items.into_iter().map(|item| item.text).collect(),
),
encoding_format: None,
dimensions,
task,
user: None,
parameters: None,
extensions: namespace_extensions("gemini", request, &["requests"]),
}),
..CanonicalRequest::default()
})
}
struct ParsedGeminiEmbeddingRequest {
model: String,
text: String,
dimensions: Option<u64>,
task: Option<String>,
}
fn parse_gemini_embedding_request_object(
request: &Map<String, Value>,
) -> Option<ParsedGeminiEmbeddingRequest> {
let model = request
.get("model")
.and_then(Value::as_str)
.map(str::trim)
.filter(|value| !value.is_empty())?
.to_string();
let parts = request
.get("content")
.and_then(Value::as_object)
.and_then(|content| content.get("parts"))
.and_then(Value::as_array)?;
let text = parts
.iter()
.map(|part| {
part.as_object()?
.get("text")?
.as_str()
.map(str::trim)
.filter(|value| !value.is_empty())
.map(ToOwned::to_owned)
})
.collect::<Option<Vec<_>>>()?
.join("\n");
if text.trim().is_empty() {
return None;
}
Some(ParsedGeminiEmbeddingRequest {
model,
text,
dimensions: request
.get("outputDimensionality")
.or_else(|| request.get("output_dimensionality"))
.and_then(Value::as_u64),
task: request
.get("taskType")
.or_else(|| request.get("task_type"))
.and_then(Value::as_str)
.map(str::trim)
.filter(|value| !value.is_empty())
.map(ToOwned::to_owned),
})
}
pub fn to(request: &CanonicalRequest, ctx: &FormatContext) -> Option<Value> {
let embedding = request.embedding.as_ref()?;
let items = embedding.input.as_string_items()?;
if items.is_empty() || items.iter().any(|value| value.trim().is_empty()) {
return None;
}
let model = mapped_embedding_model(request, ctx.mapped_model_or(request.model.as_str()));
if items.len() == 1 {
return Some(Value::Object(gemini_embedding_request_object(
&model, items[0], embedding,
)));
}
let model_resource = gemini_embedding_model_resource_name(&model);
let requests = items
.into_iter()
.map(|text| {
Value::Object(gemini_embedding_request_object(
&model_resource,
text,
embedding,
))
})
.collect::<Vec<_>>();
Some(json!({ "requests": requests }))
}
fn gemini_embedding_request_object(
model: &str,
text: &str,
embedding: &CanonicalEmbeddingRequest,
) -> Map<String, Value> {
let mut object = Map::new();
object.insert("model".to_string(), Value::String(model.to_string()));
object.insert(
"content".to_string(),
json!({
"parts": [{"text": text}]
}),
);
insert_gemini_embedding_options(&mut object, embedding);
object
}
fn gemini_embedding_model_resource_name(model: &str) -> String {
let trimmed = model.trim();
if trimmed.starts_with("models/") {
trimmed.to_string()
} else {
format!("models/{trimmed}")
}
}
fn insert_gemini_embedding_options(
object: &mut Map<String, Value>,
embedding: &CanonicalEmbeddingRequest,
) {
if let Some(dimensions) = embedding.dimensions {
object.insert("outputDimensionality".to_string(), Value::from(dimensions));
}
if let Some(task_type) = embedding
.task
.as_deref()
.and_then(normalize_gemini_embedding_task_type)
{
object.insert("taskType".to_string(), Value::String(task_type));
}
}
fn normalize_gemini_embedding_task_type(value: &str) -> Option<String> {
let normalized = value.trim();
if normalized.is_empty() {
return None;
}
let key = normalized.replace(['-', ' '], "_").to_ascii_uppercase();
let task_type = match key.as_str() {
"QUERY" | "RETRIEVAL_QUERY" => "RETRIEVAL_QUERY",
"DOCUMENT" | "RETRIEVAL_DOCUMENT" => "RETRIEVAL_DOCUMENT",
"TEXT_MATCHING" | "SEMANTIC_SIMILARITY" => "SEMANTIC_SIMILARITY",
"CLASSIFICATION" => "CLASSIFICATION",
"CLUSTERING" => "CLUSTERING",
"QUESTION_ANSWERING" => "QUESTION_ANSWERING",
"FACT_VERIFICATION" => "FACT_VERIFICATION",
"CODE_RETRIEVAL_QUERY" => "CODE_RETRIEVAL_QUERY",
_ => key.as_str(),
};
Some(task_type.to_string())
}
#[cfg(test)]
mod tests {
use std::collections::BTreeMap;
use serde_json::json;
use super::to;
use crate::formats::context::FormatContext;
use crate::protocol::canonical::{
CanonicalEmbeddingInput, CanonicalEmbeddingRequest, CanonicalRequest,
};
fn canonical_embedding(input: CanonicalEmbeddingInput) -> CanonicalRequest {
CanonicalRequest {
model: "text-embedding-3-small".to_string(),
embedding: Some(CanonicalEmbeddingRequest {
input,
encoding_format: None,
dimensions: None,
task: None,
user: None,
parameters: None,
extensions: BTreeMap::new(),
}),
..CanonicalRequest::default()
}
}
#[test]
fn single_string_array_item_uses_single_embed_content_body() {
let request = canonical_embedding(CanonicalEmbeddingInput::StringArray(vec![
"hello".to_string()
]));
let body = to(
&request,
&FormatContext::default().with_mapped_model("gemini-embedding-2-preview"),
)
.expect("gemini embedding request");
assert_eq!(body["model"], "gemini-embedding-2-preview");
assert_eq!(body["content"]["parts"][0]["text"], "hello");
assert!(body.get("requests").is_none());
}
#[test]
fn multiple_string_items_use_gemini_batch_request_body() {
let request = canonical_embedding(CanonicalEmbeddingInput::StringArray(vec![
"alpha".to_string(),
"beta".to_string(),
]));
let body = to(
&request,
&FormatContext::default().with_mapped_model("gemini-embedding-2-preview"),
)
.expect("gemini embedding request");
assert!(body.get("model").is_none());
assert_eq!(body["requests"].as_array().map(Vec::len), Some(2));
assert_eq!(
body["requests"][0]["model"],
"models/gemini-embedding-2-preview"
);
assert_eq!(body["requests"][0]["content"]["parts"][0]["text"], "alpha");
assert_eq!(
body["requests"][1]["model"],
"models/gemini-embedding-2-preview"
);
assert_eq!(body["requests"][1]["content"]["parts"][0]["text"], "beta");
}
#[test]
fn explicit_embedding_options_are_preserved_without_defaults() {
let mut request = canonical_embedding(CanonicalEmbeddingInput::String("query".to_string()));
let embedding = request.embedding.as_mut().expect("embedding request");
embedding.dimensions = Some(768);
embedding.task = Some("retrieval_query".to_string());
let body = to(
&request,
&FormatContext::default().with_mapped_model("gemini-embedding-2-preview"),
)
.expect("gemini embedding request");
assert_eq!(body["outputDimensionality"], json!(768));
assert_eq!(body["taskType"], "RETRIEVAL_QUERY");
}
}
@@ -0,0 +1,158 @@
use serde_json::Value;
use crate::formats::openai::embedding::request::namespace_extensions;
use crate::protocol::canonical::{
gemini_usage_to_canonical, CanonicalEmbedding, CanonicalEmbeddingResponse,
};
pub fn from(body_json: &Value) -> Option<CanonicalEmbeddingResponse> {
let body = body_json.as_object()?;
if body.contains_key("error") {
return None;
}
let embeddings = if let Some(raw_embeddings) = vertex_predict_embeddings(body) {
raw_embeddings
} else if let Some(values) = body
.get("embedding")
.and_then(Value::as_object)
.and_then(|embedding| embedding.get("values"))
.and_then(Value::as_array)
{
vec![CanonicalEmbedding {
index: 0,
embedding: embedding_values(values)?,
extensions: Default::default(),
}]
} else {
let raw_embeddings = body.get("embeddings")?.as_array()?;
raw_embeddings
.iter()
.enumerate()
.map(|(index, item)| {
let item_object = item.as_object()?;
let values = item_object.get("values")?.as_array()?;
Some(CanonicalEmbedding {
index,
embedding: embedding_values(values)?,
extensions: namespace_extensions("gemini", item_object, &["values"]),
})
})
.collect::<Option<Vec<_>>>()?
};
if embeddings.is_empty()
|| embeddings
.iter()
.any(|embedding| embedding.embedding.is_empty())
{
return None;
}
Some(CanonicalEmbeddingResponse {
id: body
.get("id")
.or_else(|| body.get("responseId"))
.and_then(Value::as_str)
.unwrap_or("embd-gemini-unknown")
.to_string(),
model: body
.get("model")
.or_else(|| body.get("modelVersion"))
.or_else(|| body.get("deployedModelId"))
.and_then(Value::as_str)
.unwrap_or("unknown")
.to_string(),
embeddings,
usage: gemini_usage_to_canonical(body.get("usageMetadata")),
extensions: namespace_extensions(
"gemini",
body,
&[
"id",
"responseId",
"model",
"modelVersion",
"deployedModelId",
"embedding",
"embeddings",
"predictions",
"usageMetadata",
],
),
})
}
fn vertex_predict_embeddings(
body: &serde_json::Map<String, Value>,
) -> Option<Vec<CanonicalEmbedding>> {
let predictions = body.get("predictions")?.as_array()?;
predictions
.iter()
.enumerate()
.map(|(index, item)| {
let item_object = item.as_object()?;
let embedding_object = item_object.get("embeddings")?.as_object()?;
let values = embedding_object.get("values")?.as_array()?;
Some(CanonicalEmbedding {
index,
embedding: embedding_values(values)?,
extensions: namespace_extensions("vertex", item_object, &["embeddings"]),
})
})
.collect()
}
fn embedding_values(values: &[Value]) -> Option<Vec<f64>> {
values.iter().map(Value::as_f64).collect()
}
#[cfg(test)]
mod tests {
use serde_json::json;
use super::from;
#[test]
fn parses_gemini_single_embedding_response() {
let body = json!({
"embedding": {"values": [0.1, 0.2, 0.3]},
"usageMetadata": {
"promptTokenCount": 4,
"totalTokenCount": 4
}
});
let parsed = from(&body).expect("response should parse");
assert_eq!(parsed.model, "unknown");
assert_eq!(parsed.embeddings[0].embedding, vec![0.1, 0.2, 0.3]);
let usage = parsed.usage.expect("usage should parse");
assert_eq!(usage.input_tokens, 4);
assert_eq!(usage.total_tokens, 4);
}
#[test]
fn parses_vertex_predict_embedding_response() {
let body = json!({
"predictions": [
{
"embeddings": {
"values": [0.1, 0.2, 0.3]
}
},
{
"embeddings": {
"values": [0.4, 0.5, 0.6]
}
}
],
"deployedModelId": "gemini-embedding-2"
});
let parsed = from(&body).expect("response should parse");
assert_eq!(parsed.model, "gemini-embedding-2");
assert_eq!(parsed.embeddings[0].embedding, vec![0.1, 0.2, 0.3]);
assert_eq!(parsed.embeddings[1].embedding, vec![0.4, 0.5, 0.6]);
}
}
@@ -0,0 +1 @@
pub mod spec;
@@ -0,0 +1,69 @@
use crate::contracts::{
GEMINI_FILES_DELETE_PLAN_KIND, GEMINI_FILES_DOWNLOAD_PLAN_KIND, GEMINI_FILES_GET_PLAN_KIND,
GEMINI_FILES_LIST_PLAN_KIND, GEMINI_FILES_UPLOAD_PLAN_KIND,
};
#[derive(Debug, Clone, Copy)]
pub struct LocalGeminiFilesSpec {
pub decision_kind: &'static str,
pub report_kind: Option<&'static str>,
pub require_streaming: bool,
}
pub fn resolve_sync_spec(plan_kind: &str) -> Option<LocalGeminiFilesSpec> {
match plan_kind {
GEMINI_FILES_UPLOAD_PLAN_KIND => Some(LocalGeminiFilesSpec {
decision_kind: GEMINI_FILES_UPLOAD_PLAN_KIND,
report_kind: Some("gemini_files_store_mapping"),
require_streaming: false,
}),
GEMINI_FILES_LIST_PLAN_KIND => Some(LocalGeminiFilesSpec {
decision_kind: GEMINI_FILES_LIST_PLAN_KIND,
report_kind: Some("gemini_files_store_mapping"),
require_streaming: false,
}),
GEMINI_FILES_GET_PLAN_KIND => Some(LocalGeminiFilesSpec {
decision_kind: GEMINI_FILES_GET_PLAN_KIND,
report_kind: Some("gemini_files_store_mapping"),
require_streaming: false,
}),
GEMINI_FILES_DELETE_PLAN_KIND => Some(LocalGeminiFilesSpec {
decision_kind: GEMINI_FILES_DELETE_PLAN_KIND,
report_kind: Some("gemini_files_delete_mapping"),
require_streaming: false,
}),
_ => None,
}
}
pub fn resolve_stream_spec(plan_kind: &str) -> Option<LocalGeminiFilesSpec> {
match plan_kind {
GEMINI_FILES_DOWNLOAD_PLAN_KIND => Some(LocalGeminiFilesSpec {
decision_kind: GEMINI_FILES_DOWNLOAD_PLAN_KIND,
report_kind: None,
require_streaming: true,
}),
_ => None,
}
}
#[cfg(test)]
mod tests {
use super::{resolve_stream_spec, resolve_sync_spec};
#[test]
fn resolves_sync_gemini_files_specs() {
let spec = resolve_sync_spec("gemini_files_upload").expect("spec");
assert_eq!(spec.decision_kind, "gemini_files_upload");
assert_eq!(spec.report_kind, Some("gemini_files_store_mapping"));
assert!(!spec.require_streaming);
}
#[test]
fn resolves_stream_gemini_files_spec() {
let spec = resolve_stream_spec("gemini_files_download").expect("spec");
assert_eq!(spec.decision_kind, "gemini_files_download");
assert_eq!(spec.report_kind, None);
assert!(spec.require_streaming);
}
}
@@ -0,0 +1,53 @@
use crate::contracts::{GEMINI_CHAT_STREAM_PLAN_KIND, GEMINI_CHAT_SYNC_PLAN_KIND};
use crate::formats::shared::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);
}
}
@@ -0,0 +1,53 @@
use crate::contracts::{GEMINI_CLI_STREAM_PLAN_KIND, GEMINI_CLI_SYNC_PLAN_KIND};
use crate::formats::shared::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);
}
}
@@ -0,0 +1,16 @@
pub mod chat_spec;
pub mod cli_spec;
pub mod request;
pub mod response;
pub mod spec;
pub mod stream;
use crate::formats::shared::family::LocalStandardSpec;
pub fn resolve_sync_spec(plan_kind: &str) -> Option<LocalStandardSpec> {
chat_spec::resolve_sync_spec(plan_kind).or_else(|| cli_spec::resolve_sync_spec(plan_kind))
}
pub fn resolve_stream_spec(plan_kind: &str) -> Option<LocalStandardSpec> {
chat_spec::resolve_stream_spec(plan_kind).or_else(|| cli_spec::resolve_stream_spec(plan_kind))
}
@@ -0,0 +1,847 @@
use std::collections::BTreeMap;
use serde_json::{json, Map, Value};
use crate::{
formats::{
context::FormatContext,
openai::shared::{
map_openai_reasoning_effort_to_gemini_budget,
map_thinking_budget_to_openai_reasoning_effort,
},
shared::model_directives::{gemini_model_uses_thinking_level, ReasoningEffort},
},
protocol::canonical::{
apply_gemini_request_extensions, canonical_extension_object_mut,
canonical_openai_reasoning_effort, extract_gemini_model_from_path,
gemini_contents_to_canonical_messages, gemini_extensions, gemini_generation_config,
gemini_generation_config_extra, gemini_google_search_grounding,
gemini_response_format_to_canonical, gemini_system_to_canonical_instructions,
gemini_thinking_to_canonical, gemini_tool_choice_to_canonical, gemini_tools_to_canonical,
gemini_value_by_case, CanonicalContentBlock, CanonicalMessage, CanonicalRequest,
CanonicalResponseFormat, CanonicalRole, CanonicalToolChoice, CanonicalToolDefinition,
OPENAI_RESPONSES_LEGACY_EXTENSION_NAMESPACE,
},
};
pub fn from(body: &Value, ctx: &FormatContext) -> Option<CanonicalRequest> {
from_raw(body, ctx.request_path.as_deref().unwrap_or_default())
}
pub fn to(request: &CanonicalRequest, ctx: &FormatContext) -> Option<Value> {
to_raw(
request,
ctx.mapped_model_or(request.model.as_str()),
ctx.upstream_is_stream,
)
}
pub fn from_raw(body_json: &Value, request_path: &str) -> Option<CanonicalRequest> {
let request = body_json.as_object()?;
let mut canonical = CanonicalRequest {
model: request
.get("model")
.and_then(Value::as_str)
.map(str::trim)
.filter(|value| !value.is_empty())
.map(ToOwned::to_owned)
.or_else(|| extract_gemini_model_from_path(request_path))
.unwrap_or_default(),
..CanonicalRequest::default()
};
canonical.instructions = gemini_system_to_canonical_instructions(
request
.get("systemInstruction")
.or_else(|| request.get("system_instruction")),
)?;
let system_text = canonical
.instructions
.iter()
.map(|instruction| instruction.text.as_str())
.filter(|text| !text.trim().is_empty())
.collect::<Vec<_>>()
.join("\n\n");
if !system_text.is_empty() {
canonical.system = Some(system_text);
}
canonical.messages = gemini_contents_to_canonical_messages(request.get("contents"))?;
canonical.generation = gemini_generation_config(
request
.get("generationConfig")
.or_else(|| request.get("generation_config")),
);
canonical.thinking = gemini_thinking_to_canonical(
request
.get("generationConfig")
.or_else(|| request.get("generation_config")),
);
canonical.response_format = gemini_response_format_to_canonical(
request
.get("generationConfig")
.or_else(|| request.get("generation_config")),
);
let (tools, builtin_tools, web_search_options, raw_tools, google_search_grounding) =
gemini_tools_to_canonical(request.get("tools"))?;
canonical.tools = tools;
canonical.tool_choice = gemini_tool_choice_to_canonical(
request
.get("toolConfig")
.or_else(|| request.get("tool_config")),
);
canonical.extensions = gemini_extensions(
request,
&[
"model",
"systemInstruction",
"system_instruction",
"contents",
"generationConfig",
"generation_config",
"tools",
"toolConfig",
"tool_config",
"safetySettings",
"safety_settings",
"cachedContent",
"cached_content",
"stream",
],
);
if let Some(generation_config) = request
.get("generationConfig")
.or_else(|| request.get("generation_config"))
.and_then(Value::as_object)
{
let gemini_extension = canonical_extension_object_mut(&mut canonical.extensions, "gemini");
if let Some(thinking_config) =
gemini_value_by_case(generation_config, "thinkingConfig", "thinking_config").cloned()
{
gemini_extension.insert("thinking_config".to_string(), thinking_config);
}
if let Some(response_modalities) = gemini_value_by_case(
generation_config,
"responseModalities",
"response_modalities",
)
.cloned()
{
gemini_extension.insert("response_modalities".to_string(), response_modalities);
}
let extra = gemini_generation_config_extra(generation_config);
if !extra.is_empty() {
gemini_extension.insert("generation_config_extra".to_string(), Value::Object(extra));
}
}
if let Some(value) = request
.get("safetySettings")
.or_else(|| request.get("safety_settings"))
.cloned()
{
canonical_extension_object_mut(&mut canonical.extensions, "gemini")
.insert("safety_settings".to_string(), value);
}
if let Some(value) = request
.get("cachedContent")
.or_else(|| request.get("cached_content"))
.cloned()
{
canonical_extension_object_mut(&mut canonical.extensions, "gemini")
.insert("cached_content".to_string(), value);
}
if let Some(raw_tools) = raw_tools {
canonical_extension_object_mut(&mut canonical.extensions, "gemini")
.insert("raw_tools".to_string(), raw_tools);
}
if !builtin_tools.is_empty() {
canonical_extension_object_mut(&mut canonical.extensions, "gemini")
.insert("builtin_tools".to_string(), Value::Array(builtin_tools));
}
if let Some(google_search_grounding) = google_search_grounding {
let gemini_extension = canonical_extension_object_mut(&mut canonical.extensions, "gemini");
gemini_extension.insert(
"grounding".to_string(),
json!({ "google_search": google_search_grounding }),
);
}
if let Some(tool_config) = request
.get("toolConfig")
.or_else(|| request.get("tool_config"))
.cloned()
{
canonical_extension_object_mut(&mut canonical.extensions, "gemini")
.insert("raw_tool_config".to_string(), tool_config);
}
if let Some(web_search_options) = web_search_options {
canonical_extension_object_mut(&mut canonical.extensions, "openai")
.insert("web_search_options".to_string(), web_search_options);
}
Some(canonical)
}
pub fn to_raw(
canonical: &CanonicalRequest,
mapped_model: &str,
upstream_is_stream: bool,
) -> Option<Value> {
let mut output = canonical_to_gemini_request_body(canonical, mapped_model, upstream_is_stream)?;
apply_gemini_request_extensions(&mut output, &canonical.extensions)?;
Some(output)
}
fn canonical_to_gemini_request_body(
canonical: &CanonicalRequest,
mapped_model: &str,
_upstream_is_stream: bool,
) -> Option<Value> {
let mut output = Map::new();
if !mapped_model.trim().is_empty() {
output.insert(
"model".to_string(),
Value::String(mapped_model.trim().to_string()),
);
}
output.insert(
"contents".to_string(),
Value::Array(compact_gemini_contents(
canonical_messages_to_gemini_contents(&canonical.messages)?,
)),
);
if let Some(system_instruction) = canonical_system_instruction(canonical) {
output.insert("systemInstruction".to_string(), system_instruction);
}
if let Some(generation_config) = canonical_generation_config_to_gemini(canonical, mapped_model)
{
output.insert("generationConfig".to_string(), generation_config);
}
if let Some(tools) = canonical_tools_to_gemini(canonical) {
output.insert("tools".to_string(), tools);
}
if let Some(tool_config) = canonical_tool_choice_to_gemini(canonical.tool_choice.as_ref()) {
output.insert("toolConfig".to_string(), tool_config);
}
Some(Value::Object(output))
}
fn canonical_system_instruction(canonical: &CanonicalRequest) -> Option<Value> {
let text = canonical
.instructions
.iter()
.map(|instruction| instruction.text.as_str())
.filter(|text| !text.trim().is_empty())
.collect::<Vec<_>>()
.join("\n\n");
let text = if text.trim().is_empty() {
canonical.system.as_deref().unwrap_or_default().to_string()
} else {
text
};
(!text.trim().is_empty()).then(|| json!({ "parts": [{ "text": text }] }))
}
fn canonical_messages_to_gemini_contents(messages: &[CanonicalMessage]) -> Option<Vec<Value>> {
let mut contents = Vec::new();
let mut tool_name_by_id = BTreeMap::new();
for message in messages {
let role = match message.role {
CanonicalRole::Assistant => "model",
CanonicalRole::System | CanonicalRole::Developer => continue,
CanonicalRole::Tool | CanonicalRole::User | CanonicalRole::Unknown => "user",
};
let parts = canonical_blocks_to_gemini_parts(&message.content, &mut tool_name_by_id)?;
if parts.is_empty() {
continue;
}
contents.push(json!({
"role": role,
"parts": parts,
}));
}
Some(contents)
}
fn canonical_blocks_to_gemini_parts(
blocks: &[CanonicalContentBlock],
tool_name_by_id: &mut BTreeMap<String, String>,
) -> Option<Vec<Value>> {
let mut parts = Vec::new();
for block in blocks {
if let Some(part) = canonical_block_to_gemini_part(block, tool_name_by_id)? {
parts.push(part);
}
}
Some(parts)
}
fn canonical_block_to_gemini_part(
block: &CanonicalContentBlock,
tool_name_by_id: &mut BTreeMap<String, String>,
) -> Option<Option<Value>> {
match block {
CanonicalContentBlock::Text { text, .. } => Some(Some(json!({ "text": text }))),
CanonicalContentBlock::Thinking {
text, signature, ..
} => {
if text.trim().is_empty() {
return Some(None);
}
let mut part = Map::new();
part.insert("text".to_string(), Value::String(text.clone()));
part.insert("thought".to_string(), Value::Bool(true));
if let Some(signature) = signature.as_ref().filter(|value| !value.is_empty()) {
part.insert(
"thoughtSignature".to_string(),
Value::String(signature.clone()),
);
}
Some(Some(Value::Object(part)))
}
CanonicalContentBlock::Image {
data,
url,
media_type,
..
} => Some(Some(canonical_media_to_gemini_part(
media_type.as_deref().unwrap_or("image/png"),
data.as_deref(),
url.as_deref(),
))),
CanonicalContentBlock::File {
data,
file_url,
media_type,
..
} => Some(Some(canonical_media_to_gemini_part(
media_type.as_deref().unwrap_or("application/octet-stream"),
data.as_deref(),
file_url.as_deref(),
))),
CanonicalContentBlock::Audio {
data, media_type, ..
} => Some(data.as_ref().map(|data| {
json!({
"inlineData": {
"mimeType": media_type.clone().unwrap_or_else(|| "audio/mpeg".to_string()),
"data": data,
}
})
})),
CanonicalContentBlock::ToolUse {
id, name, input, ..
} => {
tool_name_by_id.insert(id.clone(), name.clone());
Some(Some(json!({
"functionCall": {
"id": id,
"name": name,
"args": gemini_function_args(input),
}
})))
}
CanonicalContentBlock::ToolResult {
tool_use_id,
name,
output,
content_text,
..
} => Some(Some(json!({
"functionResponse": {
"id": tool_use_id,
"name": name.clone()
.or_else(|| tool_name_by_id.get(tool_use_id).cloned())
.unwrap_or_else(|| tool_use_id.clone()),
"response": gemini_function_response(output.as_ref(), content_text.as_deref()),
}
}))),
CanonicalContentBlock::Unknown { .. } => Some(None),
}
}
fn canonical_media_to_gemini_part(
media_type: &str,
data: Option<&str>,
url: Option<&str>,
) -> Value {
if let Some(data) = data.filter(|value| !value.is_empty()) {
return json!({
"inlineData": {
"mimeType": media_type,
"data": data,
}
});
}
json!({
"fileData": {
"mimeType": media_type,
"fileUri": url.unwrap_or_default(),
}
})
}
fn canonical_generation_config_to_gemini(
canonical: &CanonicalRequest,
mapped_model: &str,
) -> Option<Value> {
let mut generation_config = Map::new();
if let Some(value) = canonical.generation.max_tokens {
generation_config.insert("maxOutputTokens".to_string(), Value::from(value));
}
insert_f64(
&mut generation_config,
"temperature",
canonical.generation.temperature,
);
insert_f64(&mut generation_config, "topP", canonical.generation.top_p);
if let Some(value) = canonical.generation.top_k {
generation_config.insert("topK".to_string(), Value::from(value));
}
if let Some(value) = canonical.generation.n.filter(|value| *value > 1) {
generation_config.insert("candidateCount".to_string(), Value::from(value));
}
if let Some(value) = canonical.generation.seed {
generation_config.insert("seed".to_string(), Value::from(value));
}
if let Some(stop_sequences) = &canonical.generation.stop_sequences {
generation_config.insert(
"stopSequences".to_string(),
Value::Array(stop_sequences.iter().cloned().map(Value::String).collect()),
);
}
if let Some(response_format) = &canonical.response_format {
apply_response_format_to_gemini_generation_config(&mut generation_config, response_format);
}
if let Some(thinking_config) = canonical.thinking.as_ref().and_then(|thinking| {
thinking
.extensions
.get("gemini")
.and_then(|value| value.get("thinking_config"))
.cloned()
.or_else(|| {
let effort = canonical_openai_reasoning_effort(thinking);
gemini_thinking_config_from_reasoning(mapped_model, effort, thinking.budget_tokens)
})
}) {
generation_config.insert("thinkingConfig".to_string(), thinking_config);
}
(!generation_config.is_empty()).then_some(Value::Object(generation_config))
}
fn gemini_thinking_config_from_reasoning(
mapped_model: &str,
effort: Option<&str>,
budget_tokens: Option<u64>,
) -> Option<Value> {
if gemini_model_uses_thinking_level(mapped_model) {
let level = effort
.and_then(ReasoningEffort::parse)
.or_else(|| {
budget_tokens
.map(map_thinking_budget_to_openai_reasoning_effort)
.and_then(ReasoningEffort::parse)
})
.map(ReasoningEffort::as_gemini_level_value)?;
return Some(json!({
"includeThoughts": true,
"thinkingLevel": level,
}));
}
let budget =
budget_tokens.or_else(|| effort.and_then(map_openai_reasoning_effort_to_gemini_budget))?;
Some(json!({
"includeThoughts": true,
"thinkingBudget": budget,
}))
}
fn apply_response_format_to_gemini_generation_config(
generation_config: &mut Map<String, Value>,
response_format: &CanonicalResponseFormat,
) {
match response_format.format_type.as_str() {
"json_schema" => {
generation_config.insert(
"responseMimeType".to_string(),
Value::String("application/json".to_string()),
);
if let Some(schema) = response_format
.json_schema
.as_ref()
.and_then(|value| value.get("schema"))
.cloned()
.or_else(|| response_format.json_schema.clone())
{
let mut schema = schema;
clean_gemini_schema(&mut schema);
generation_config.insert("responseSchema".to_string(), schema);
}
}
"json_object" => {
generation_config.insert(
"responseMimeType".to_string(),
Value::String("application/json".to_string()),
);
}
_ => {}
}
}
fn canonical_tools_to_gemini(canonical: &CanonicalRequest) -> Option<Value> {
let mut declarations = Vec::new();
let mut tools = Vec::new();
let mut google_search = canonical
.extensions
.get("openai")
.and_then(Value::as_object)
.is_some_and(|value| value.contains_key("web_search_options"));
let mut google_search_payload = canonical_google_search_output_payload(canonical);
if google_search_payload.is_some() {
google_search = true;
}
let mut code_execution = false;
let mut url_context = false;
for tool in &canonical.tools {
match normalize_gemini_builtin_tool_name(&tool.name) {
Some("googleSearch") => {
google_search = true;
continue;
}
Some("codeExecution") => {
code_execution = true;
continue;
}
Some("urlContext") => {
url_context = true;
continue;
}
Some(_) => continue,
None => {}
}
if tool
.extensions
.get("openai_responses")
.or_else(|| {
tool.extensions
.get(OPENAI_RESPONSES_LEGACY_EXTENSION_NAMESPACE)
})
.and_then(|value| value.get("type"))
.and_then(Value::as_str)
.is_some_and(|tool_type| tool_type.starts_with("web_search"))
{
google_search = true;
continue;
}
declarations.push(canonical_tool_to_gemini_declaration(tool));
}
let mut emitted_google_search = false;
let mut emitted_code_execution = false;
let mut emitted_url_context = false;
if let Some(builtin_tools) = canonical
.extensions
.get("gemini")
.and_then(Value::as_object)
.and_then(|value| value.get("builtin_tools"))
.and_then(Value::as_array)
{
for builtin_tool in builtin_tools {
let Some(tool_object) = builtin_tool.as_object() else {
tools.push(builtin_tool.clone());
continue;
};
let mut emitted_builtin_portion = false;
if let Some(grounding) = gemini_google_search_grounding(tool_object) {
google_search = true;
if google_search_payload.is_none() {
google_search_payload = Some(grounding.output_payload);
}
if !emitted_google_search {
tools.push(json!({
"googleSearch": google_search_payload.clone().unwrap_or_else(|| json!({}))
}));
emitted_google_search = true;
}
emitted_builtin_portion = true;
}
if let Some(tool) =
gemini_builtin_tool_by_case(tool_object, "codeExecution", "code_execution")
{
if !emitted_code_execution {
tools.push(tool);
emitted_code_execution = true;
}
emitted_builtin_portion = true;
}
if let Some(tool) =
gemini_builtin_tool_by_case(tool_object, "urlContext", "url_context")
{
if !emitted_url_context {
tools.push(tool);
emitted_url_context = true;
}
emitted_builtin_portion = true;
}
if let Some(tool) = gemini_unhandled_builtin_tool_portion(tool_object) {
tools.push(tool);
} else if !emitted_builtin_portion {
tools.push(builtin_tool.clone());
}
}
}
if code_execution && !emitted_code_execution {
tools.push(json!({ "codeExecution": {} }));
}
if google_search && !emitted_google_search {
tools.push(json!({
"googleSearch": google_search_payload.unwrap_or_else(|| json!({}))
}));
}
if url_context && !emitted_url_context {
tools.push(json!({ "urlContext": {} }));
}
if !declarations.is_empty() {
tools.push(json!({ "functionDeclarations": declarations }));
}
(!tools.is_empty()).then_some(Value::Array(tools))
}
fn canonical_google_search_output_payload(canonical: &CanonicalRequest) -> Option<Value> {
let google_search = canonical
.extensions
.get("gemini")
.and_then(Value::as_object)
.and_then(|value| value.get("grounding"))
.and_then(Value::as_object)
.and_then(|value| value.get("google_search"))
.and_then(Value::as_object)?;
google_search
.get("legacy")
.and_then(Value::as_bool)
.filter(|legacy| *legacy)
.map(|_| json!({}))
.or_else(|| google_search.get("payload").cloned())
}
fn gemini_builtin_tool_by_case(
tool_object: &Map<String, Value>,
camel: &str,
snake: &str,
) -> Option<Value> {
let payload = tool_object
.get(camel)
.or_else(|| tool_object.get(snake))
.map(gemini_builtin_tool_payload)?;
Some(json!({ camel: payload }))
}
fn gemini_builtin_tool_payload(payload: &Value) -> Value {
match payload {
Value::Null => json!({}),
value => value.clone(),
}
}
fn gemini_unhandled_builtin_tool_portion(tool_object: &Map<String, Value>) -> Option<Value> {
let builtin = tool_object
.iter()
.filter(|(key, _)| {
!matches!(
key.as_str(),
"googleSearch"
| "google_search"
| "googleSearchRetrieval"
| "google_search_retrieval"
| "codeExecution"
| "code_execution"
| "urlContext"
| "url_context"
)
})
.map(|(key, value)| (key.clone(), value.clone()))
.collect::<Map<_, _>>();
(!builtin.is_empty()).then_some(Value::Object(builtin))
}
fn canonical_tool_to_gemini_declaration(tool: &CanonicalToolDefinition) -> Value {
let mut declaration = Map::new();
declaration.insert("name".to_string(), Value::String(tool.name.clone()));
if let Some(description) = &tool.description {
declaration.insert(
"description".to_string(),
Value::String(description.clone()),
);
}
let raw_parameters = tool
.extensions
.get("gemini")
.and_then(Value::as_object)
.and_then(|value| value.get("raw_parameters"))
.cloned();
declaration.insert(
"parameters".to_string(),
raw_parameters
.clone()
.or_else(|| tool.parameters.clone())
.map(|mut schema| {
if raw_parameters.is_none() {
clean_gemini_schema(&mut schema);
}
schema
})
.unwrap_or_else(|| json!({})),
);
Value::Object(declaration)
}
fn canonical_tool_choice_to_gemini(choice: Option<&CanonicalToolChoice>) -> Option<Value> {
let choice = choice?;
let mode = match choice {
CanonicalToolChoice::Auto => "AUTO",
CanonicalToolChoice::None => "NONE",
CanonicalToolChoice::Required | CanonicalToolChoice::Tool { .. } => "ANY",
};
let mut function_calling_config = Map::new();
function_calling_config.insert("mode".to_string(), Value::String(mode.to_string()));
if let CanonicalToolChoice::Tool { name } = choice {
function_calling_config.insert(
"allowedFunctionNames".to_string(),
Value::Array(vec![Value::String(name.clone())]),
);
}
Some(json!({
"functionCallingConfig": Value::Object(function_calling_config),
}))
}
fn gemini_function_args(input: &Value) -> Value {
match input {
Value::Object(_) => input.clone(),
Value::Null => json!({}),
other => json!({ "value": other.clone() }),
}
}
fn gemini_function_response(output: Option<&Value>, content_text: Option<&str>) -> Value {
match output {
Some(value) => json!({ "result": value }),
None => json!({ "result": content_text.unwrap_or_default() }),
}
}
fn compact_gemini_contents(contents: Vec<Value>) -> Vec<Value> {
let mut compact: Vec<Value> = Vec::new();
for content in contents {
let role = content
.get("role")
.and_then(Value::as_str)
.unwrap_or_default()
.to_string();
let parts = content
.get("parts")
.and_then(Value::as_array)
.cloned()
.unwrap_or_default();
if parts.is_empty() {
continue;
}
if let Some(last) = compact.last_mut() {
let last_role = last.get("role").and_then(Value::as_str).unwrap_or_default();
if last_role == role {
if let Some(last_parts) = last
.as_object_mut()
.and_then(|object| object.get_mut("parts"))
.and_then(Value::as_array_mut)
{
last_parts.extend(parts);
continue;
}
}
}
compact.push(json!({
"role": role,
"parts": parts,
}));
}
compact
}
fn normalize_gemini_builtin_tool_name(name: &str) -> Option<&'static str> {
match name
.trim()
.replace(['_', '-', ' '], "")
.to_ascii_lowercase()
.as_str()
{
"googlesearch" | "websearch" | "websearchpreview" => Some("googleSearch"),
"codeexecution" => Some("codeExecution"),
"urlcontext" => Some("urlContext"),
_ => None,
}
}
fn insert_f64(output: &mut Map<String, Value>, key: &str, value: Option<f64>) {
if let Some(value) = value.and_then(serde_json::Number::from_f64) {
output.insert(key.to_string(), Value::Number(value));
}
}
fn clean_gemini_schema(value: &mut Value) {
match value {
Value::Object(object) => {
for inner in object.values_mut() {
clean_gemini_schema(inner);
}
if object.get("type").and_then(Value::as_str) == Some("object")
&& !object.contains_key("properties")
{
object.insert("properties".to_string(), Value::Object(Map::new()));
}
}
Value::Array(items) => {
for item in items {
clean_gemini_schema(item);
}
}
_ => {}
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::CanonicalContentBlock;
#[test]
fn canonical_tool_result_to_gemini_request_preserves_function_response_id() {
let mut tool_name_by_id = BTreeMap::new();
tool_name_by_id.insert("call_1".to_string(), "lookup".to_string());
let part = canonical_block_to_gemini_part(
&CanonicalContentBlock::ToolResult {
tool_use_id: "call_1".to_string(),
name: None,
output: Some(serde_json::json!({"ok": true})),
content_text: None,
is_error: false,
extensions: BTreeMap::new(),
},
&mut tool_name_by_id,
)
.expect("part should be representable")
.expect("part should not be omitted");
let function_response = part
.get("functionResponse")
.and_then(Value::as_object)
.expect("functionResponse should exist");
assert_eq!(function_response["id"], "call_1");
assert_eq!(function_response["name"], "lookup");
assert_eq!(
function_response["response"],
serde_json::json!({"result": {"ok": true}})
);
}
}
@@ -0,0 +1,474 @@
use serde_json::{json, Map, Value};
use crate::{
formats::context::FormatContext,
protocol::canonical::{
canonical_extension_object_mut, canonical_usage_total_input_tokens,
canonical_usage_total_tokens_for_inclusive_input, gemini_extensions,
gemini_part_to_canonical_block, gemini_stop_reason_to_canonical, gemini_usage_to_canonical,
CanonicalContentBlock, CanonicalResponse, CanonicalResponseOutput, CanonicalRole,
CanonicalStopReason, CanonicalUsage,
},
};
pub fn from(body: &Value, _ctx: &FormatContext) -> Option<CanonicalResponse> {
from_raw(body)
}
pub fn to(response: &CanonicalResponse, ctx: &FormatContext) -> Option<Value> {
to_raw(response, &ctx.report_context_value())
}
pub fn from_raw(body_json: &Value) -> Option<CanonicalResponse> {
let body = body_json.as_object()?;
if body.contains_key("error") {
return None;
}
let candidates = body.get("candidates")?.as_array()?;
let mut outputs = Vec::new();
for (fallback_index, candidate) in candidates.iter().enumerate() {
let candidate_object = candidate.as_object()?;
let parts = candidate_object
.get("content")
.and_then(Value::as_object)
.and_then(|content| content.get("parts"))
.and_then(Value::as_array)
.map(Vec::as_slice)
.unwrap_or(&[]);
let content = parts
.iter()
.enumerate()
.filter_map(|(index, part)| gemini_part_to_canonical_block(part, index))
.collect::<Vec<_>>();
let mut stop_reason = candidate_object
.get("finishReason")
.or_else(|| candidate_object.get("finish_reason"))
.and_then(Value::as_str)
.and_then(gemini_stop_reason_to_canonical);
if content
.iter()
.any(|block| matches!(block, CanonicalContentBlock::ToolUse { .. }))
&& stop_reason
.as_ref()
.is_none_or(|reason| matches!(reason, CanonicalStopReason::EndTurn))
{
stop_reason = Some(CanonicalStopReason::ToolUse);
}
let mut extensions = gemini_extensions(
candidate_object,
&["index", "content", "finishReason", "finish_reason"],
);
if let Some(raw_finish_reason) = candidate_object
.get("finishReason")
.or_else(|| candidate_object.get("finish_reason"))
.cloned()
{
canonical_extension_object_mut(&mut extensions, "gemini")
.insert("raw_finish_reason".to_string(), raw_finish_reason);
}
outputs.push(CanonicalResponseOutput {
index: candidate_object
.get("index")
.and_then(Value::as_u64)
.and_then(|value| usize::try_from(value).ok())
.unwrap_or(fallback_index),
role: CanonicalRole::Assistant,
content,
stop_reason,
extensions,
});
}
outputs.retain(gemini_response_output_has_visible_content);
if outputs.is_empty() {
return None;
}
let content = outputs
.first()
.map(|output| output.content.clone())
.unwrap_or_default();
let stop_reason = outputs
.first()
.and_then(|output| output.stop_reason.clone());
let mut canonical = CanonicalResponse {
id: body
.get("responseId")
.or_else(|| body.get("_v1internal_response_id"))
.and_then(Value::as_str)
.unwrap_or("gemini-local-finalize")
.to_string(),
model: body
.get("modelVersion")
.and_then(Value::as_str)
.unwrap_or("unknown")
.to_string(),
outputs,
content,
stop_reason,
usage: gemini_usage_to_canonical(body.get("usageMetadata")),
extensions: gemini_extensions(
body,
&[
"responseId",
"_v1internal_response_id",
"modelVersion",
"candidates",
"usageMetadata",
],
),
};
if let Some(candidates) = body.get("candidates").cloned() {
canonical_extension_object_mut(&mut canonical.extensions, "gemini")
.insert("raw_candidates".to_string(), candidates);
}
Some(canonical)
}
fn gemini_response_output_has_visible_content(output: &CanonicalResponseOutput) -> bool {
output.content.iter().any(|block| match block {
CanonicalContentBlock::Text { text, .. } => !text.trim().is_empty(),
CanonicalContentBlock::ToolUse { .. }
| CanonicalContentBlock::ToolResult { .. }
| CanonicalContentBlock::Image { .. }
| CanonicalContentBlock::File { .. }
| CanonicalContentBlock::Audio { .. } => true,
CanonicalContentBlock::Thinking { .. } | CanonicalContentBlock::Unknown { .. } => false,
})
}
pub fn to_raw(canonical: &CanonicalResponse, report_context: &Value) -> Option<Value> {
let mut response = canonical_to_gemini_response(canonical, report_context)?;
if let Some(object) = response.as_object_mut() {
if let Some(gemini) = canonical
.extensions
.get("gemini")
.and_then(Value::as_object)
{
for (key, value) in gemini {
if key == "raw_candidates" || object.contains_key(key) {
continue;
}
object.insert(key.clone(), value.clone());
}
}
}
Some(response)
}
fn canonical_to_gemini_response(
canonical: &CanonicalResponse,
report_context: &Value,
) -> Option<Value> {
let outputs = if canonical.outputs.is_empty() {
vec![CanonicalResponseOutput {
index: 0,
role: crate::protocol::canonical::CanonicalRole::Assistant,
content: canonical.content.clone(),
stop_reason: canonical.stop_reason.clone(),
extensions: Default::default(),
}]
} else {
canonical.outputs.clone()
};
let mut candidates = Vec::new();
for output in outputs {
let parts = canonical_blocks_to_gemini_parts(&output.content)?;
let mut candidate = json!({
"index": output.index,
"content": {
"role": "model",
"parts": parts,
},
"finishReason": canonical_stop_reason_to_gemini(
output.stop_reason.as_ref().or(canonical.stop_reason.as_ref())
),
});
if let Some(candidate_object) = candidate.as_object_mut() {
if let Some(gemini) = output.extensions.get("gemini").and_then(Value::as_object) {
if let Some(raw_finish_reason) = gemini.get("raw_finish_reason").cloned() {
candidate_object.insert("finishReason".to_string(), raw_finish_reason);
}
for (key, value) in gemini {
if key == "raw_finish_reason" {
continue;
}
candidate_object.entry(key.clone()).or_insert(value.clone());
}
}
}
candidates.push(candidate);
}
let mut response = Map::new();
response.insert(
"responseId".to_string(),
Value::String(if canonical.id.trim().is_empty() {
"resp-local-finalize".to_string()
} else {
canonical.id.clone()
}),
);
response.insert(
"modelVersion".to_string(),
Value::String(
if canonical.model.trim().is_empty() || canonical.model == "unknown" {
report_context
.get("mapped_model")
.and_then(Value::as_str)
.or_else(|| report_context.get("model").and_then(Value::as_str))
.unwrap_or("unknown")
.to_string()
} else {
canonical.model.clone()
},
),
);
response.insert("candidates".to_string(), Value::Array(candidates));
if let Some(usage) = &canonical.usage {
response.insert(
"usageMetadata".to_string(),
canonical_usage_to_gemini_usage_metadata(usage),
);
}
Some(Value::Object(response))
}
fn canonical_blocks_to_gemini_parts(blocks: &[CanonicalContentBlock]) -> Option<Vec<Value>> {
let mut parts = Vec::new();
for block in blocks {
if let Some(part) = canonical_block_to_gemini_part(block)? {
parts.push(part);
}
}
if parts.is_empty() {
parts.push(json!({ "text": "" }));
}
Some(parts)
}
fn canonical_block_to_gemini_part(block: &CanonicalContentBlock) -> Option<Option<Value>> {
match block {
CanonicalContentBlock::Text { text, .. } => Some(Some(json!({ "text": text }))),
CanonicalContentBlock::Thinking {
text, signature, ..
} => {
if text.trim().is_empty() {
return Some(None);
}
let mut part = Map::new();
part.insert("text".to_string(), Value::String(text.clone()));
part.insert("thought".to_string(), Value::Bool(true));
if let Some(signature) = signature.as_ref().filter(|value| !value.is_empty()) {
part.insert(
"thoughtSignature".to_string(),
Value::String(signature.clone()),
);
}
Some(Some(Value::Object(part)))
}
CanonicalContentBlock::ToolUse {
id, name, input, ..
} => Some(Some(json!({
"functionCall": {
"id": id,
"name": name,
"args": gemini_function_args(input),
}
}))),
CanonicalContentBlock::ToolResult {
tool_use_id,
name,
output,
content_text,
..
} => Some(Some(json!({
"functionResponse": {
"id": tool_use_id,
"name": name.clone().unwrap_or_else(|| tool_use_id.clone()),
"response": gemini_function_response(output.as_ref(), content_text.as_deref()),
}
}))),
CanonicalContentBlock::Image {
data,
url,
media_type,
..
} => Some(Some(canonical_media_to_gemini_part(
media_type.as_deref().unwrap_or("image/png"),
data.as_deref(),
url.as_deref(),
))),
CanonicalContentBlock::File {
data,
file_url,
media_type,
..
} => Some(Some(canonical_media_to_gemini_part(
media_type.as_deref().unwrap_or("application/octet-stream"),
data.as_deref(),
file_url.as_deref(),
))),
CanonicalContentBlock::Audio {
data, media_type, ..
} => Some(data.as_ref().map(|data| {
json!({
"inlineData": {
"mimeType": media_type.clone().unwrap_or_else(|| "audio/mpeg".to_string()),
"data": data,
}
})
})),
CanonicalContentBlock::Unknown { .. } => Some(None),
}
}
fn canonical_media_to_gemini_part(
media_type: &str,
data: Option<&str>,
url: Option<&str>,
) -> Value {
if let Some(data) = data.filter(|value| !value.is_empty()) {
return json!({
"inlineData": {
"mimeType": media_type,
"data": data,
}
});
}
json!({
"fileData": {
"mimeType": media_type,
"fileUri": url.unwrap_or_default(),
}
})
}
fn gemini_function_args(input: &Value) -> Value {
match input {
Value::Object(_) => input.clone(),
Value::Null => json!({}),
other => json!({ "value": other.clone() }),
}
}
fn gemini_function_response(output: Option<&Value>, content_text: Option<&str>) -> Value {
match output {
Some(Value::Object(object)) => Value::Object(object.clone()),
Some(value) => json!({ "result": value }),
None => json!({ "result": content_text.unwrap_or_default() }),
}
}
fn canonical_stop_reason_to_gemini(reason: Option<&CanonicalStopReason>) -> Value {
Value::String(
match reason {
Some(CanonicalStopReason::MaxTokens) => "MAX_TOKENS",
Some(CanonicalStopReason::ContentFiltered) | Some(CanonicalStopReason::Refusal) => {
"SAFETY"
}
Some(CanonicalStopReason::Unknown) => "OTHER",
_ => "STOP",
}
.to_string(),
)
}
fn canonical_usage_to_gemini_usage_metadata(usage: &CanonicalUsage) -> Value {
let input_tokens = canonical_usage_total_input_tokens(usage);
let mut out = Map::new();
out.insert("promptTokenCount".to_string(), Value::from(input_tokens));
out.insert(
"candidatesTokenCount".to_string(),
Value::from(usage.output_tokens.saturating_sub(usage.reasoning_tokens)),
);
out.insert(
"totalTokenCount".to_string(),
Value::from(canonical_usage_total_tokens_for_inclusive_input(
usage,
input_tokens,
)),
);
if usage.cache_read_tokens > 0 {
out.insert(
"cachedContentTokenCount".to_string(),
Value::from(usage.cache_read_tokens),
);
}
if usage.reasoning_tokens > 0 {
out.insert(
"thoughtsTokenCount".to_string(),
Value::from(usage.reasoning_tokens),
);
}
Value::Object(out)
}
#[cfg(test)]
mod tests {
use super::*;
use crate::CanonicalContentBlock;
#[test]
fn gemini_response_without_visible_parts_is_not_success() {
let body = json!({
"candidates": [{
"content": {"role": "model"},
"finishReason": "MAX_TOKENS"
}],
"usageMetadata": {
"promptTokenCount": 8,
"candidatesTokenCount": 1,
"thoughtsTokenCount": 25,
"totalTokenCount": 34
},
"modelVersion": "gemini-3-flash-preview",
"responseId": "resp-empty"
});
assert!(from_raw(&body).is_none());
}
#[test]
fn gemini_response_with_only_thought_parts_is_not_success() {
let body = json!({
"candidates": [{
"content": {
"role": "model",
"parts": [{"text": "hidden plan", "thought": true}]
},
"finishReason": "MAX_TOKENS"
}],
"modelVersion": "gemini-3-flash-preview",
"responseId": "resp-thought-only"
});
assert!(from_raw(&body).is_none());
}
#[test]
fn gemini_response_with_function_call_is_visible_output() {
let body = json!({
"candidates": [{
"content": {
"role": "model",
"parts": [{
"functionCall": {
"name": "lookup",
"args": {"query": "weather"}
}
}]
},
"finishReason": "STOP"
}],
"modelVersion": "gemini-3-flash-preview",
"responseId": "resp-tool"
});
let canonical = from_raw(&body).expect("function call should be visible output");
assert!(matches!(
canonical.content.first(),
Some(CanonicalContentBlock::ToolUse { name, .. }) if name == "lookup"
));
}
}
@@ -0,0 +1 @@
pub use super::{chat_spec, cli_spec, resolve_stream_spec, resolve_sync_spec};
File diff suppressed because it is too large Load Diff
@@ -0,0 +1,4 @@
pub mod embedding;
pub mod files;
pub mod generate_content;
pub mod video;
@@ -0,0 +1 @@
pub mod spec;
@@ -0,0 +1,27 @@
use crate::contracts::GEMINI_VIDEO_CREATE_SYNC_PLAN_KIND;
use crate::formats::shared::video::{LocalVideoCreateFamily, LocalVideoCreateSpec};
pub fn resolve_sync_spec(plan_kind: &str) -> Option<LocalVideoCreateSpec> {
match plan_kind {
GEMINI_VIDEO_CREATE_SYNC_PLAN_KIND => Some(LocalVideoCreateSpec {
api_format: "gemini:video",
decision_kind: GEMINI_VIDEO_CREATE_SYNC_PLAN_KIND,
report_kind: "gemini_video_create_sync_finalize",
family: LocalVideoCreateFamily::Gemini,
}),
_ => None,
}
}
#[cfg(test)]
mod tests {
use super::{resolve_sync_spec, LocalVideoCreateFamily};
#[test]
fn resolves_gemini_video_create_spec() {
let spec = resolve_sync_spec("gemini_video_create_sync").expect("spec");
assert_eq!(spec.api_format, "gemini:video");
assert_eq!(spec.family, LocalVideoCreateFamily::Gemini);
assert_eq!(spec.report_kind, "gemini_video_create_sync_finalize");
}
}
+632
View File
@@ -0,0 +1,632 @@
//! Format identity and aliases.
use std::{fmt, str::FromStr};
#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)]
pub enum FormatFamily {
OpenAi,
Claude,
Gemini,
Jina,
Doubao,
Aliyun,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)]
pub enum FormatProfile {
Default,
Compact,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)]
pub enum FormatId {
OpenAiChat,
OpenAiResponses,
OpenAiResponsesCompact,
OpenAiSearch,
OpenAiEmbedding,
OpenAiRerank,
ClaudeMessages,
GeminiGenerateContent,
GeminiInteractions,
GeminiEmbedding,
JinaEmbedding,
JinaRerank,
DoubaoEmbedding,
AliyunMultimodalEmbedding,
}
impl FormatId {
pub fn parse(value: &str) -> Option<Self> {
value.parse().ok()
}
pub fn canonical(self) -> Self {
self
}
pub fn family(self) -> FormatFamily {
match self {
Self::OpenAiChat
| Self::OpenAiResponses
| Self::OpenAiResponsesCompact
| Self::OpenAiSearch
| Self::OpenAiEmbedding
| Self::OpenAiRerank => FormatFamily::OpenAi,
Self::ClaudeMessages => FormatFamily::Claude,
Self::GeminiGenerateContent | Self::GeminiInteractions | Self::GeminiEmbedding => {
FormatFamily::Gemini
}
Self::JinaEmbedding | Self::JinaRerank => FormatFamily::Jina,
Self::DoubaoEmbedding => FormatFamily::Doubao,
Self::AliyunMultimodalEmbedding => FormatFamily::Aliyun,
}
}
pub fn profile(self) -> FormatProfile {
match self {
Self::OpenAiResponsesCompact => FormatProfile::Compact,
_ => FormatProfile::Default,
}
}
pub fn as_str(self) -> &'static str {
match self {
Self::OpenAiChat => "openai:chat",
Self::OpenAiResponses => "openai:responses",
Self::OpenAiResponsesCompact => "openai:responses:compact",
Self::OpenAiSearch => "openai:search",
Self::OpenAiEmbedding => "openai:embedding",
Self::OpenAiRerank => "openai:rerank",
Self::ClaudeMessages => "claude:messages",
Self::GeminiGenerateContent => "gemini:generate_content",
Self::GeminiInteractions => "gemini:interactions",
Self::GeminiEmbedding => "gemini:embedding",
Self::JinaEmbedding => "jina:embedding",
Self::JinaRerank => "jina:rerank",
Self::DoubaoEmbedding => "doubao:embedding",
Self::AliyunMultimodalEmbedding => "aliyun:multimodal_embedding",
}
}
}
impl fmt::Display for FormatId {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.write_str(self.as_str())
}
}
impl FromStr for FormatId {
type Err = ();
fn from_str(value: &str) -> Result<Self, Self::Err> {
match value.trim().to_ascii_lowercase().as_str() {
"openai" | "openai:chat" | "/v1/chat/completions" => Ok(Self::OpenAiChat),
"openai:responses" | "/v1/responses" => Ok(Self::OpenAiResponses),
"openai:responses:compact" | "/v1/responses/compact" => {
Ok(Self::OpenAiResponsesCompact)
}
"openai:search" | "openai_search" | "search" | "/v1/alpha/search" => {
Ok(Self::OpenAiSearch)
}
"openai:embedding" | "/v1/embeddings" => Ok(Self::OpenAiEmbedding),
"openai:rerank" | "/v1/rerank" => Ok(Self::OpenAiRerank),
"claude:messages" | "/v1/messages" => Ok(Self::ClaudeMessages),
"gemini:generate_content" => Ok(Self::GeminiGenerateContent),
"gemini:interactions"
| "gemini:interaction"
| "gemini_interactions"
| "gemini_interaction"
| "/v1/interactions"
| "/v1beta/interactions" => Ok(Self::GeminiInteractions),
"gemini:embedding" => Ok(Self::GeminiEmbedding),
"jina:embedding" | "/jina/v1/embeddings" => Ok(Self::JinaEmbedding),
"jina:rerank" | "/jina/v1/rerank" => Ok(Self::JinaRerank),
"doubao:embedding" => Ok(Self::DoubaoEmbedding),
"aliyun:multimodal_embedding"
| "aliyun_embedding"
| "aliyun_multimodal_embedding"
| "dashscope:multimodal_embedding"
| "dashscope_embedding"
| "dashscope_multimodal_embedding" => Ok(Self::AliyunMultimodalEmbedding),
_ => Err(()),
}
}
}
pub fn normalize_api_format_alias(value: &str) -> String {
let normalized = value.trim().to_ascii_lowercase();
FormatId::parse(&normalized)
.map(|format| format.as_str().to_string())
.unwrap_or(normalized)
}
pub fn api_format_alias_matches(left: &str, right: &str) -> bool {
normalize_api_format_alias(left) == normalize_api_format_alias(right)
}
pub fn api_format_defaults_to_non_stream(value: &str) -> bool {
matches!(
normalize_api_format_alias(value).as_str(),
"openai:chat"
| "openai:responses"
| "openai:responses:compact"
| "openai:search"
| "openai:image"
| "claude:messages"
)
}
pub fn api_format_defaults_to_client_error_failover(value: &str) -> bool {
!matches!(
FormatId::parse(value).map(FormatId::canonical),
Some(FormatId::OpenAiSearch)
)
}
pub fn api_format_permission_covers(allowed_value: &str, requested_api_format: &str) -> bool {
let allowed_value = normalize_api_format_alias(allowed_value);
let requested_api_format = normalize_api_format_alias(requested_api_format);
!allowed_value.is_empty()
&& !requested_api_format.is_empty()
&& (allowed_value == requested_api_format
|| allowed_value == "openai:responses"
&& matches!(
requested_api_format.as_str(),
"openai:responses:compact" | "openai:search"
))
}
pub fn intersect_api_format_allowed_lists(left: &[String], right: &[String]) -> Vec<String> {
let mut effective = Vec::new();
for left_value in left {
for right_value in right {
let intersection = if api_format_permission_covers(right_value, left_value) {
Some(left_value)
} else if api_format_permission_covers(left_value, right_value) {
Some(right_value)
} else {
None
};
if let Some(value) = intersection {
let normalized = normalize_api_format_alias(value);
if !effective.iter().any(|item| item == &normalized) {
effective.push(normalized);
}
}
}
}
effective
}
pub fn api_format_storage_aliases(value: &str) -> Vec<String> {
match FormatId::parse(value).map(FormatId::canonical) {
Some(FormatId::AliyunMultimodalEmbedding) => vec![
"aliyun:multimodal_embedding".to_string(),
"dashscope:multimodal_embedding".to_string(),
],
_ => vec![normalize_api_format_alias(value)],
}
}
pub fn api_format_permission_storage_aliases(value: &str) -> Vec<String> {
let requested_api_format = normalize_api_format_alias(value);
let mut aliases = api_format_storage_aliases(&requested_api_format);
for allowed_api_format in [FormatId::OpenAiResponses.as_str()] {
if !api_format_permission_covers(allowed_api_format, &requested_api_format) {
continue;
}
for alias in api_format_storage_aliases(allowed_api_format) {
if !aliases.iter().any(|existing| existing == &alias) {
aliases.push(alias);
}
}
}
aliases
}
pub fn is_openai_responses_format(value: &str) -> bool {
normalize_api_format_alias(value) == "openai:responses"
}
pub fn is_openai_responses_compact_format(value: &str) -> bool {
normalize_api_format_alias(value) == "openai:responses:compact"
}
pub fn is_openai_responses_family_format(value: &str) -> bool {
matches!(
normalize_api_format_alias(value).as_str(),
"openai:responses" | "openai:responses:compact"
)
}
pub fn api_format_uses_body_stream_field(value: &str) -> bool {
matches!(
FormatId::parse(value).map(FormatId::canonical),
Some(
FormatId::OpenAiChat
| FormatId::OpenAiResponses
| FormatId::ClaudeMessages
| FormatId::GeminiInteractions,
)
)
}
#[cfg(test)]
mod tests {
use super::{
api_format_alias_matches, api_format_defaults_to_client_error_failover,
api_format_defaults_to_non_stream, api_format_permission_covers,
api_format_permission_storage_aliases, api_format_storage_aliases,
api_format_uses_body_stream_field, intersect_api_format_allowed_lists,
normalize_api_format_alias, FormatId,
};
#[test]
fn retired_api_formats_do_not_parse() {
assert_eq!(FormatId::parse("openai:cli"), None);
assert_eq!(FormatId::parse("openai:compact"), None);
assert_eq!(FormatId::parse("claude:chat"), None);
assert_eq!(FormatId::parse("claude:cli"), None);
assert_eq!(FormatId::parse("gemini:chat"), None);
assert_eq!(FormatId::parse("gemini:cli"), None);
}
#[test]
fn responses_permission_covers_its_companion_endpoints() {
assert!(api_format_permission_covers(
"OPENAI:RESPONSES",
"openai:search"
));
assert!(api_format_permission_covers(
"OPENAI:RESPONSES",
"openai:responses:compact"
));
assert!(api_format_permission_covers(
"openai:search",
"openai:search"
));
assert!(!api_format_permission_covers(
"openai:search",
"openai:responses"
));
assert!(!api_format_permission_covers(
"openai:responses:compact",
"openai:responses"
));
assert!(!api_format_permission_covers(
"openai:responses",
"openai:chat"
));
assert_eq!(
api_format_permission_storage_aliases("openai:search"),
vec!["openai:search".to_string(), "openai:responses".to_string()]
);
assert_eq!(
api_format_permission_storage_aliases("openai:responses:compact"),
vec![
"openai:responses:compact".to_string(),
"openai:responses".to_string(),
]
);
assert_eq!(
api_format_permission_storage_aliases("openai:responses"),
vec!["openai:responses".to_string()]
);
}
#[test]
fn normalizes_openai_search_aliases() {
for alias in [
"openai:search",
"OPENAI_SEARCH",
"search",
"/v1/alpha/search",
] {
assert_eq!(FormatId::parse(alias), Some(FormatId::OpenAiSearch));
assert_eq!(normalize_api_format_alias(alias), "openai:search");
}
assert!(!api_format_uses_body_stream_field("openai:search"));
}
#[test]
fn identifies_default_non_stream_formats_from_aliases() {
for format in [
"/v1/chat/completions",
"/v1/responses",
"/v1/responses/compact",
"/v1/alpha/search",
"openai:image",
"/v1/messages",
] {
assert!(api_format_defaults_to_non_stream(format), "{format}");
}
assert!(!api_format_defaults_to_non_stream("gemini:interactions"));
}
#[test]
fn search_defaults_to_passthrough_for_client_errors() {
for format in ["openai:search", "OPENAI_SEARCH", "/v1/alpha/search"] {
assert!(
!api_format_defaults_to_client_error_failover(format),
"{format}"
);
}
assert!(api_format_defaults_to_client_error_failover(
"openai:responses"
));
assert!(api_format_defaults_to_client_error_failover(
"custom:unknown"
));
}
#[test]
fn api_format_policy_intersection_keeps_the_narrowest_companion_scope() {
assert_eq!(
intersect_api_format_allowed_lists(
&["openai:responses".to_string()],
&["openai:search".to_string()],
),
vec!["openai:search".to_string()]
);
assert_eq!(
intersect_api_format_allowed_lists(
&["openai:responses".to_string()],
&["openai:responses:compact".to_string()],
),
vec!["openai:responses:compact".to_string()]
);
assert_eq!(
intersect_api_format_allowed_lists(
&["openai:search".to_string()],
&["OPENAI:RESPONSES".to_string()],
),
vec!["openai:search".to_string()]
);
assert_eq!(
intersect_api_format_allowed_lists(
&["openai:responses".to_string()],
&["openai:responses:compact".to_string()],
),
vec!["openai:responses:compact".to_string()]
);
assert!(intersect_api_format_allowed_lists(
&["openai:search".to_string()],
&["openai:chat".to_string()],
)
.is_empty());
}
#[test]
fn parses_embedding_api_formats() {
assert_eq!(
FormatId::parse("openai:embedding"),
Some(FormatId::OpenAiEmbedding)
);
assert_eq!(
FormatId::parse("/v1/embeddings"),
Some(FormatId::OpenAiEmbedding)
);
assert_eq!(
FormatId::parse("gemini:embedding"),
Some(FormatId::GeminiEmbedding)
);
assert_eq!(
FormatId::parse("jina:embedding"),
Some(FormatId::JinaEmbedding)
);
assert_eq!(
FormatId::parse("/jina/v1/embeddings"),
Some(FormatId::JinaEmbedding)
);
assert_eq!(
FormatId::parse("doubao:embedding"),
Some(FormatId::DoubaoEmbedding)
);
assert_eq!(
FormatId::parse("aliyun:multimodal_embedding").map(|format| format.to_string()),
Some("aliyun:multimodal_embedding".to_string())
);
assert_eq!(
FormatId::parse("dashscope:multimodal_embedding").map(|format| format.to_string()),
Some("aliyun:multimodal_embedding".to_string())
);
assert_eq!(
FormatId::parse("dashscope_embedding").map(|format| format.to_string()),
Some("aliyun:multimodal_embedding".to_string())
);
assert_eq!(FormatId::OpenAiEmbedding.to_string(), "openai:embedding");
}
#[test]
fn embedding_format_ids_keep_provider_family_and_default_profile() {
use super::{FormatFamily, FormatProfile};
for (format, family) in [
(FormatId::OpenAiEmbedding, FormatFamily::OpenAi),
(FormatId::GeminiEmbedding, FormatFamily::Gemini),
(FormatId::JinaEmbedding, FormatFamily::Jina),
(FormatId::DoubaoEmbedding, FormatFamily::Doubao),
(FormatId::AliyunMultimodalEmbedding, FormatFamily::Aliyun),
] {
assert_eq!(format.family(), family);
assert_eq!(format.profile(), FormatProfile::Default);
assert_eq!(FormatId::parse(format.as_str()), Some(format));
assert_eq!(format.to_string(), format.as_str());
}
}
#[test]
fn parses_gemini_interactions_api_formats() {
use super::{FormatFamily, FormatProfile};
assert_eq!(
FormatId::parse("gemini:interactions"),
Some(FormatId::GeminiInteractions)
);
assert_eq!(
FormatId::parse("gemini_interactions"),
Some(FormatId::GeminiInteractions)
);
assert_eq!(
FormatId::parse("/v1/interactions"),
Some(FormatId::GeminiInteractions)
);
assert_eq!(
FormatId::GeminiInteractions.to_string(),
"gemini:interactions"
);
assert_eq!(FormatId::GeminiInteractions.family(), FormatFamily::Gemini);
assert_eq!(
FormatId::GeminiInteractions.profile(),
FormatProfile::Default
);
}
#[test]
fn parses_rerank_api_formats() {
assert_eq!(
FormatId::parse("openai:rerank"),
Some(FormatId::OpenAiRerank)
);
assert_eq!(FormatId::parse("/v1/rerank"), Some(FormatId::OpenAiRerank));
assert_eq!(FormatId::parse("jina:rerank"), Some(FormatId::JinaRerank));
assert_eq!(
FormatId::parse("/jina/v1/rerank"),
Some(FormatId::JinaRerank)
);
assert_eq!(FormatId::OpenAiRerank.to_string(), "openai:rerank");
}
#[test]
fn rerank_format_ids_keep_provider_family_and_default_profile() {
use super::{FormatFamily, FormatProfile};
for (format, family) in [
(FormatId::OpenAiRerank, FormatFamily::OpenAi),
(FormatId::JinaRerank, FormatFamily::Jina),
] {
assert_eq!(format.family(), family);
assert_eq!(format.profile(), FormatProfile::Default);
assert_eq!(FormatId::parse(format.as_str()), Some(format));
assert_eq!(format.to_string(), format.as_str());
}
}
#[test]
fn rejects_unknown_embedding_format() {
assert_eq!(FormatId::parse("embedding"), None);
assert_eq!(FormatId::parse("openai:embeddings"), None);
assert_eq!(FormatId::parse("claude:embedding"), None);
assert_eq!(FormatId::parse("gemini:embed_content"), None);
}
#[test]
fn normalizes_api_format_aliases() {
assert_eq!(
normalize_api_format_alias(" OPENAI:RESPONSES "),
"openai:responses"
);
assert_eq!(
normalize_api_format_alias("OPENAI:RESPONSES:COMPACT"),
"openai:responses:compact"
);
assert_eq!(
normalize_api_format_alias("CLAUDE:MESSAGES"),
"claude:messages"
);
assert_eq!(
normalize_api_format_alias("GEMINI:GENERATE_CONTENT"),
"gemini:generate_content"
);
assert_eq!(
normalize_api_format_alias("GEMINI_INTERACTIONS"),
"gemini:interactions"
);
assert_eq!(
normalize_api_format_alias("OPENAI:EMBEDDING"),
"openai:embedding"
);
assert_eq!(normalize_api_format_alias("openai:image"), "openai:image");
assert_eq!(normalize_api_format_alias("openai:video"), "openai:video");
assert_eq!(normalize_api_format_alias("gemini:video"), "gemini:video");
assert_eq!(normalize_api_format_alias("gemini:files"), "gemini:files");
assert!(!api_format_alias_matches("claude:cli", "claude:messages"));
assert!(!api_format_alias_matches(
"gemini:chat",
"gemini:generate_content"
));
assert!(!api_format_alias_matches("openai:cli", "openai:responses"));
assert_eq!(
normalize_api_format_alias("openai:compact"),
"openai:compact"
);
}
#[test]
fn storage_aliases_only_include_normalized_value() {
assert_eq!(
api_format_storage_aliases("openai:responses"),
vec!["openai:responses".to_string()]
);
assert_eq!(
api_format_storage_aliases("openai:responses:compact"),
vec!["openai:responses:compact".to_string()]
);
assert_eq!(
api_format_storage_aliases("claude:messages"),
vec!["claude:messages".to_string()]
);
assert_eq!(
api_format_storage_aliases("gemini:generate_content"),
vec!["gemini:generate_content".to_string()]
);
assert_eq!(
api_format_storage_aliases("gemini:interactions"),
vec!["gemini:interactions".to_string()]
);
assert_eq!(
api_format_storage_aliases("openai:embedding"),
vec!["openai:embedding".to_string()]
);
assert_eq!(
api_format_storage_aliases("gemini:embedding"),
vec!["gemini:embedding".to_string()]
);
assert_eq!(
api_format_storage_aliases("jina:embedding"),
vec!["jina:embedding".to_string()]
);
assert_eq!(
api_format_storage_aliases("doubao:embedding"),
vec!["doubao:embedding".to_string()]
);
assert_eq!(
api_format_storage_aliases("dashscope:multimodal_embedding"),
vec![
"aliyun:multimodal_embedding".to_string(),
"dashscope:multimodal_embedding".to_string(),
]
);
}
#[test]
fn body_stream_field_support_matches_provider_wire_formats() {
assert!(api_format_uses_body_stream_field("openai:chat"));
assert!(api_format_uses_body_stream_field("/v1/chat/completions"));
assert!(api_format_uses_body_stream_field("openai:responses"));
assert!(api_format_uses_body_stream_field("/v1/responses"));
assert!(api_format_uses_body_stream_field("claude:messages"));
assert!(api_format_uses_body_stream_field("/v1/messages"));
assert!(api_format_uses_body_stream_field("gemini:interactions"));
assert!(api_format_uses_body_stream_field("/v1/interactions"));
assert!(!api_format_uses_body_stream_field(
"openai:responses:compact"
));
assert!(!api_format_uses_body_stream_field("/v1/responses/compact"));
assert!(!api_format_uses_body_stream_field(
"gemini:generate_content"
));
assert!(!api_format_uses_body_stream_field("openai:embedding"));
}
}
@@ -0,0 +1,2 @@
pub mod request;
pub mod response;
@@ -0,0 +1,17 @@
use serde_json::Value;
use crate::formats::context::FormatContext;
use crate::protocol::canonical::CanonicalRequest;
pub fn from(body: &Value, _ctx: &FormatContext) -> Option<CanonicalRequest> {
crate::formats::openai::embedding::request::from_namespace(body, "jina")
}
pub fn to(request: &CanonicalRequest, ctx: &FormatContext) -> Option<Value> {
crate::formats::openai::embedding::request::to_openai_like(
request,
ctx.mapped_model_or(request.model.as_str()),
"jina",
true,
)
}
@@ -0,0 +1,13 @@
use serde_json::Value;
use crate::protocol::canonical::CanonicalEmbeddingResponse;
pub fn from(body: &Value) -> Option<CanonicalEmbeddingResponse> {
crate::formats::openai::embedding::response::from_namespace(body, "jina")
}
pub fn to(response: &CanonicalEmbeddingResponse) -> Option<Value> {
Some(crate::formats::openai::embedding::response::to_openai_like(
response, "jina",
))
}
@@ -0,0 +1,2 @@
pub mod embedding;
pub mod rerank;
@@ -0,0 +1 @@
pub mod request;
@@ -0,0 +1,16 @@
use serde_json::Value;
use crate::formats::context::FormatContext;
use crate::protocol::canonical::CanonicalRequest;
pub fn from(body: &Value, _ctx: &FormatContext) -> Option<CanonicalRequest> {
crate::formats::openai::rerank::request::from_namespace(body, "jina")
}
pub fn to(request: &CanonicalRequest, ctx: &FormatContext) -> Option<Value> {
crate::formats::openai::rerank::request::to_openai_like(
request,
ctx.mapped_model_or(request.model.as_str()),
"jina",
)
}
@@ -0,0 +1,772 @@
use crate::{
api_format_alias_matches,
formats::id::{is_openai_responses_compact_format, normalize_api_format_alias},
};
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum RequestConversionKind {
ToOpenAIChat,
ToOpenAiResponses,
ToClaudeStandard,
ToGeminiStandard,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum SyncChatResponseConversionKind {
ToOpenAIChat,
ToClaudeChat,
ToGeminiChat,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum SyncCliResponseConversionKind {
ToOpenAiResponses,
ToClaudeCli,
ToGeminiCli,
}
const NON_COMPACT_STANDARD_CANDIDATE_API_FORMATS: &[&str] = &[
"openai:chat",
"openai:responses",
"claude:messages",
"gemini:generate_content",
];
const STANDARD_API_FORMAT_ORDER: &[&str] = &[
"openai:chat",
"openai:responses",
"claude:messages",
"gemini:generate_content",
];
const EMBEDDING_CANDIDATE_API_FORMATS: &[&str] = &[
"openai:embedding",
"gemini:embedding",
"jina:embedding",
"doubao:embedding",
"aliyun:multimodal_embedding",
];
const RERANK_CANDIDATE_API_FORMATS: &[&str] = &["openai:rerank", "jina:rerank"];
const GEMINI_INTERACTIONS_CANDIDATE_API_FORMATS: &[&str] = &["gemini:interactions"];
pub fn request_candidate_api_format_preference(
client_api_format: &str,
provider_api_format: &str,
) -> Option<(u8, u8)> {
let client_api_format = normalize_api_format_alias(client_api_format);
let provider_api_format = normalize_api_format_alias(provider_api_format);
if client_api_format == "openai:responses:compact" {
return (provider_api_format == "openai:responses:compact").then_some((0, 0));
}
if client_api_format == "openai:search" {
return (provider_api_format == "openai:search").then_some((0, 0));
}
if is_gemini_interactions_api_format(client_api_format.as_str()) {
return (provider_api_format == "gemini:interactions").then_some((0, 0));
}
if is_embedding_api_format(client_api_format.as_str()) {
return is_embedding_api_format(provider_api_format.as_str()).then_some((
if client_api_format == provider_api_format {
0
} else {
1
},
embedding_api_format_priority(provider_api_format.as_str()),
));
}
if is_rerank_api_format(client_api_format.as_str()) {
return is_rerank_api_format(provider_api_format.as_str()).then_some((
if client_api_format == provider_api_format {
0
} else {
1
},
rerank_api_format_priority(provider_api_format.as_str()),
));
}
let (client_family, client_kind) =
parse_non_compact_standard_api_format(client_api_format.as_str())?;
let (provider_family, provider_kind) =
parse_non_compact_standard_api_format(provider_api_format.as_str())?;
let preference_bucket = if client_api_format == provider_api_format {
0
} else if client_kind == provider_kind {
1
} else if client_family == provider_family {
2
} else {
3
};
Some((
preference_bucket,
standard_api_format_priority(provider_api_format.as_str()),
))
}
pub fn request_candidate_api_formats(
client_api_format: &str,
_require_streaming: bool,
) -> Vec<&'static str> {
let client_api_format = normalize_api_format_alias(client_api_format);
if client_api_format == "openai:responses:compact" {
return vec!["openai:responses:compact"];
}
if client_api_format == "openai:search" {
return vec!["openai:search"];
}
if is_gemini_interactions_api_format(client_api_format.as_str()) {
return GEMINI_INTERACTIONS_CANDIDATE_API_FORMATS.to_vec();
}
if is_embedding_api_format(client_api_format.as_str()) {
let mut candidate_api_formats = EMBEDDING_CANDIDATE_API_FORMATS.to_vec();
candidate_api_formats.sort_by_key(|provider_api_format| {
request_candidate_api_format_preference(client_api_format.as_str(), provider_api_format)
.unwrap_or((u8::MAX, u8::MAX))
});
return candidate_api_formats;
}
if is_rerank_api_format(client_api_format.as_str()) {
let mut candidate_api_formats = RERANK_CANDIDATE_API_FORMATS.to_vec();
candidate_api_formats.sort_by_key(|provider_api_format| {
request_candidate_api_format_preference(client_api_format.as_str(), provider_api_format)
.unwrap_or((u8::MAX, u8::MAX))
});
return candidate_api_formats;
}
if parse_non_compact_standard_api_format(client_api_format.as_str()).is_none() {
return Vec::new();
}
let mut candidate_api_formats = NON_COMPACT_STANDARD_CANDIDATE_API_FORMATS.to_vec();
candidate_api_formats.sort_by_key(|provider_api_format| {
request_candidate_api_format_preference(client_api_format.as_str(), provider_api_format)
.unwrap_or((u8::MAX, u8::MAX))
});
candidate_api_formats
}
pub fn request_conversion_kind(
client_api_format: &str,
provider_api_format: &str,
) -> Option<RequestConversionKind> {
let client_api_format = normalize_api_format_alias(client_api_format);
let provider_api_format = normalize_api_format_alias(provider_api_format);
if client_api_format == provider_api_format {
return None;
}
if !is_standard_api_format(client_api_format.as_str())
|| !is_standard_api_format(provider_api_format.as_str())
{
return None;
}
if is_openai_responses_compact_format(client_api_format.as_str())
|| is_openai_responses_compact_format(provider_api_format.as_str())
{
return None;
}
match provider_api_format.as_str() {
"openai:chat" => Some(RequestConversionKind::ToOpenAIChat),
"openai:responses" => Some(RequestConversionKind::ToOpenAiResponses),
"claude:messages" => Some(RequestConversionKind::ToClaudeStandard),
"gemini:generate_content" => Some(RequestConversionKind::ToGeminiStandard),
_ => None,
}
}
pub fn sync_chat_response_conversion_kind(
provider_api_format: &str,
client_api_format: &str,
) -> Option<SyncChatResponseConversionKind> {
let provider_api_format = normalize_api_format_alias(provider_api_format);
let client_api_format = normalize_api_format_alias(client_api_format);
if provider_api_format == client_api_format {
return None;
}
if !is_standard_api_format(provider_api_format.as_str()) {
return None;
}
request_conversion_kind(client_api_format.as_str(), provider_api_format.as_str())?;
match client_api_format.as_str() {
"openai:chat" => Some(SyncChatResponseConversionKind::ToOpenAIChat),
"claude:messages" => Some(SyncChatResponseConversionKind::ToClaudeChat),
"gemini:generate_content" => Some(SyncChatResponseConversionKind::ToGeminiChat),
_ => None,
}
}
pub fn sync_cli_response_conversion_kind(
provider_api_format: &str,
client_api_format: &str,
) -> Option<SyncCliResponseConversionKind> {
let provider_api_format = normalize_api_format_alias(provider_api_format);
let client_api_format = normalize_api_format_alias(client_api_format);
if provider_api_format == client_api_format {
return None;
}
if !is_standard_api_format(provider_api_format.as_str()) {
return None;
}
if !is_openai_responses_compact_format(client_api_format.as_str()) {
request_conversion_kind(client_api_format.as_str(), provider_api_format.as_str())?;
}
match client_api_format.as_str() {
"openai:responses" | "openai:responses:compact" => {
Some(SyncCliResponseConversionKind::ToOpenAiResponses)
}
"claude:messages" => Some(SyncCliResponseConversionKind::ToClaudeCli),
"gemini:generate_content" => Some(SyncCliResponseConversionKind::ToGeminiCli),
_ => None,
}
}
pub fn request_conversion_requires_enable_flag(
client_api_format: &str,
provider_api_format: &str,
) -> bool {
let client_api_format = normalize_api_format_alias(client_api_format);
let provider_api_format = normalize_api_format_alias(provider_api_format);
match (
api_data_format_id(client_api_format.as_str()),
api_data_format_id(provider_api_format.as_str()),
) {
(Some(client_data_format), Some(provider_data_format)) => {
client_data_format != provider_data_format
}
_ => true,
}
}
pub fn is_standard_api_format(api_format: &str) -> bool {
matches!(
normalize_api_format_alias(api_format).as_str(),
"openai:chat"
| "openai:responses"
| "openai:responses:compact"
| "claude:messages"
| "gemini:generate_content"
)
}
pub fn is_embedding_api_format(api_format: &str) -> bool {
matches!(
normalize_api_format_alias(api_format).as_str(),
"openai:embedding"
| "gemini:embedding"
| "jina:embedding"
| "doubao:embedding"
| "aliyun:multimodal_embedding"
)
}
pub fn is_rerank_api_format(api_format: &str) -> bool {
matches!(
normalize_api_format_alias(api_format).as_str(),
"openai:rerank" | "jina:rerank"
)
}
pub fn is_gemini_interactions_api_format(api_format: &str) -> bool {
normalize_api_format_alias(api_format) == "gemini:interactions"
}
pub fn parse_non_compact_standard_api_format(
api_format: &str,
) -> Option<(&'static str, &'static str)> {
match normalize_api_format_alias(api_format).as_str() {
"openai:chat" => Some(("openai", "chat")),
"openai:responses" => Some(("openai", "responses")),
"claude:messages" => Some(("claude", "messages")),
"gemini:generate_content" => Some(("gemini", "generate_content")),
_ => None,
}
}
pub fn api_data_format_id(api_format: &str) -> Option<&'static str> {
match normalize_api_format_alias(api_format).as_str() {
"claude:messages" => Some("claude"),
"gemini:generate_content" => Some("gemini"),
"gemini:interactions" => Some("gemini_interactions"),
"openai:chat" => Some("openai_chat"),
"openai:responses" | "openai:responses:compact" => Some("openai_responses"),
"openai:embedding"
| "gemini:embedding"
| "jina:embedding"
| "doubao:embedding"
| "aliyun:multimodal_embedding" => Some("embedding"),
"openai:rerank" | "jina:rerank" => Some("rerank"),
_ => None,
}
}
pub fn normalized_same_standard_api_format(left: &str, right: &str) -> bool {
api_format_alias_matches(left, right)
}
fn standard_api_format_priority(api_format: &str) -> u8 {
let api_format = normalize_api_format_alias(api_format);
STANDARD_API_FORMAT_ORDER
.iter()
.position(|candidate| *candidate == api_format)
.unwrap_or(STANDARD_API_FORMAT_ORDER.len()) as u8
}
fn embedding_api_format_priority(api_format: &str) -> u8 {
let api_format = normalize_api_format_alias(api_format);
EMBEDDING_CANDIDATE_API_FORMATS
.iter()
.position(|candidate| *candidate == api_format)
.unwrap_or(EMBEDDING_CANDIDATE_API_FORMATS.len()) as u8
}
fn rerank_api_format_priority(api_format: &str) -> u8 {
let api_format = normalize_api_format_alias(api_format);
RERANK_CANDIDATE_API_FORMATS
.iter()
.position(|candidate| *candidate == api_format)
.unwrap_or(RERANK_CANDIDATE_API_FORMATS.len()) as u8
}
#[cfg(test)]
mod tests {
use super::{
api_data_format_id, is_embedding_api_format, is_gemini_interactions_api_format,
is_rerank_api_format, request_candidate_api_format_preference,
request_candidate_api_formats, request_conversion_kind,
request_conversion_requires_enable_flag, sync_chat_response_conversion_kind,
sync_cli_response_conversion_kind, RequestConversionKind, SyncChatResponseConversionKind,
SyncCliResponseConversionKind,
};
fn expected_request_conversion_kind(provider_api_format: &str) -> RequestConversionKind {
match provider_api_format {
"openai:chat" => RequestConversionKind::ToOpenAIChat,
"openai:responses" => RequestConversionKind::ToOpenAiResponses,
"claude:messages" => RequestConversionKind::ToClaudeStandard,
"gemini:generate_content" => RequestConversionKind::ToGeminiStandard,
other => panic!("unexpected provider format {other}"),
}
}
#[test]
fn request_conversion_registry_supports_bidirectional_standard_matrix() {
assert_eq!(
request_conversion_kind("openai:chat", "openai:responses"),
Some(RequestConversionKind::ToOpenAiResponses)
);
assert_eq!(
request_conversion_kind("openai:chat", "claude:messages"),
Some(RequestConversionKind::ToClaudeStandard)
);
assert_eq!(
request_conversion_kind("openai:responses", "openai:chat"),
Some(RequestConversionKind::ToOpenAIChat)
);
assert_eq!(
request_conversion_kind("openai:responses:compact", "gemini:generate_content"),
None
);
assert_eq!(
request_conversion_kind("gemini:generate_content", "openai:responses:compact"),
None
);
assert_eq!(
request_conversion_kind("openai:chat", "openai:responses:compact"),
None
);
assert_eq!(
request_conversion_kind("openai:responses", "openai:cli"),
None
);
assert_eq!(
request_conversion_kind("openai:compact", "openai:responses:compact"),
None
);
assert_eq!(
request_conversion_kind("openai:search", "openai:responses"),
None
);
assert_eq!(
request_conversion_kind("gemini:generate_content", "claude:messages"),
Some(RequestConversionKind::ToClaudeStandard)
);
assert_eq!(request_conversion_kind("claude:chat", "claude:cli"), None);
assert_eq!(
request_conversion_kind("claude:messages", "claude:messages"),
None
);
let formats = [
"openai:chat",
"openai:responses",
"claude:messages",
"gemini:generate_content",
];
for client_api_format in formats {
for provider_api_format in formats {
let actual = request_conversion_kind(client_api_format, provider_api_format);
if client_api_format == provider_api_format {
assert_eq!(actual, None, "{client_api_format} -> {provider_api_format}");
} else {
assert_eq!(
actual,
Some(expected_request_conversion_kind(provider_api_format)),
"{client_api_format} -> {provider_api_format}"
);
}
}
}
}
#[test]
fn search_candidate_registry_keeps_exact_protocol_identity() {
assert_eq!(
request_candidate_api_formats("openai:search", false),
vec!["openai:search"]
);
assert_eq!(
request_candidate_api_formats("/v1/alpha/search", true),
vec!["openai:search"]
);
assert_eq!(
request_candidate_api_format_preference("openai:search", "openai:search"),
Some((0, 0))
);
assert_eq!(
request_candidate_api_format_preference("openai:search", "openai:responses"),
None
);
}
#[test]
fn sync_response_conversion_registry_supports_bidirectional_standard_matrix() {
assert_eq!(
sync_chat_response_conversion_kind("openai:chat", "claude:messages"),
Some(SyncChatResponseConversionKind::ToClaudeChat)
);
assert_eq!(
sync_chat_response_conversion_kind("claude:messages", "gemini:generate_content"),
Some(SyncChatResponseConversionKind::ToGeminiChat)
);
assert_eq!(
sync_chat_response_conversion_kind("gemini:generate_content", "openai:chat"),
Some(SyncChatResponseConversionKind::ToOpenAIChat)
);
assert_eq!(
sync_cli_response_conversion_kind("openai:responses", "gemini:generate_content"),
Some(SyncCliResponseConversionKind::ToGeminiCli)
);
assert_eq!(
sync_cli_response_conversion_kind("claude:messages", "openai:responses"),
Some(SyncCliResponseConversionKind::ToOpenAiResponses)
);
assert_eq!(
sync_cli_response_conversion_kind("claude:messages", "openai:responses:compact"),
Some(SyncCliResponseConversionKind::ToOpenAiResponses)
);
assert_eq!(
sync_cli_response_conversion_kind("openai:responses:compact", "claude:messages"),
None
);
assert_eq!(
sync_cli_response_conversion_kind("gemini:generate_content", "claude:messages"),
Some(SyncCliResponseConversionKind::ToClaudeCli)
);
assert_eq!(
sync_cli_response_conversion_kind("openai:responses", "openai:cli"),
None
);
assert_eq!(
sync_cli_response_conversion_kind("openai:compact", "openai:responses:compact"),
None
);
}
#[test]
fn embedding_candidate_registry_excludes_chat_generation_formats() {
assert_eq!(
request_candidate_api_formats("openai:embedding", false),
vec![
"openai:embedding",
"gemini:embedding",
"jina:embedding",
"doubao:embedding",
"aliyun:multimodal_embedding",
]
);
assert_eq!(
request_candidate_api_formats("jina:embedding", false),
vec![
"jina:embedding",
"openai:embedding",
"gemini:embedding",
"doubao:embedding",
"aliyun:multimodal_embedding",
]
);
assert!(!request_candidate_api_formats("openai:embedding", false).contains(&"openai:chat"));
assert!(!request_candidate_api_formats("openai:embedding", false)
.contains(&"gemini:generate_content"));
assert_eq!(
request_conversion_kind("openai:embedding", "jina:embedding"),
None
);
assert_eq!(
request_conversion_kind("openai:embedding", "openai:chat"),
None
);
assert!(!request_conversion_requires_enable_flag(
"openai:embedding",
"jina:embedding"
));
}
#[test]
fn embedding_candidate_registry_covers_all_provider_orderings() {
assert_eq!(
request_candidate_api_formats("gemini:embedding", true),
vec![
"gemini:embedding",
"openai:embedding",
"jina:embedding",
"doubao:embedding",
"aliyun:multimodal_embedding",
]
);
assert_eq!(
request_candidate_api_formats("doubao:embedding", false),
vec![
"doubao:embedding",
"openai:embedding",
"gemini:embedding",
"jina:embedding",
"aliyun:multimodal_embedding",
]
);
assert_eq!(
request_candidate_api_formats("aliyun:multimodal_embedding", false),
vec![
"aliyun:multimodal_embedding",
"openai:embedding",
"gemini:embedding",
"jina:embedding",
"doubao:embedding",
]
);
let embedding_formats = [
"openai:embedding",
"gemini:embedding",
"jina:embedding",
"doubao:embedding",
"aliyun:multimodal_embedding",
];
for client_api_format in embedding_formats {
for provider_api_format in embedding_formats {
assert!(
request_candidate_api_format_preference(client_api_format, provider_api_format)
.is_some(),
"{client_api_format} should consider {provider_api_format} as embedding candidate"
);
assert_eq!(
request_conversion_kind(client_api_format, provider_api_format),
None,
"embedding pair should not use chat/generation conversion kind"
);
}
}
}
#[test]
fn embedding_candidate_registry_never_crosses_chat_generation_boundary() {
let embedding_formats = [
"openai:embedding",
"gemini:embedding",
"jina:embedding",
"doubao:embedding",
"aliyun:multimodal_embedding",
];
let standard_formats = [
"openai:chat",
"openai:responses",
"claude:messages",
"gemini:generate_content",
];
for embedding_api_format in embedding_formats {
assert!(is_embedding_api_format(embedding_api_format));
assert_eq!(api_data_format_id(embedding_api_format), Some("embedding"));
for standard_api_format in standard_formats {
assert_eq!(
request_candidate_api_format_preference(
embedding_api_format,
standard_api_format
),
None
);
assert_eq!(
request_candidate_api_format_preference(
standard_api_format,
embedding_api_format
),
None
);
assert_eq!(
request_conversion_kind(embedding_api_format, standard_api_format),
None
);
assert_eq!(
request_conversion_kind(standard_api_format, embedding_api_format),
None
);
}
}
}
#[test]
fn gemini_interactions_stays_same_format_only() {
assert!(is_gemini_interactions_api_format("gemini:interactions"));
assert_eq!(
request_candidate_api_formats("gemini:interactions", false),
vec!["gemini:interactions"]
);
assert_eq!(
request_candidate_api_format_preference("gemini:interactions", "gemini:interactions"),
Some((0, 0))
);
assert_eq!(
request_candidate_api_format_preference(
"gemini:interactions",
"gemini:generate_content"
),
None
);
assert_eq!(
request_conversion_kind("gemini:interactions", "gemini:generate_content"),
None
);
assert_eq!(
request_conversion_kind("gemini:generate_content", "gemini:interactions"),
None
);
assert_eq!(
api_data_format_id("gemini:interactions"),
Some("gemini_interactions")
);
}
#[test]
fn rerank_candidate_registry_excludes_chat_and_embedding_formats() {
assert_eq!(
request_candidate_api_formats("openai:rerank", false),
vec!["openai:rerank", "jina:rerank"]
);
assert_eq!(
request_candidate_api_formats("jina:rerank", false),
vec!["jina:rerank", "openai:rerank"]
);
assert_eq!(api_data_format_id("openai:rerank"), Some("rerank"));
assert!(is_rerank_api_format("jina:rerank"));
assert!(!is_embedding_api_format("openai:rerank"));
assert_eq!(
request_candidate_api_format_preference("openai:rerank", "openai:embedding"),
None
);
assert_eq!(
request_candidate_api_format_preference("openai:rerank", "openai:chat"),
None
);
assert_eq!(
request_conversion_kind("openai:rerank", "jina:rerank"),
None
);
assert!(!request_conversion_requires_enable_flag(
"openai:rerank",
"jina:rerank"
));
}
#[test]
fn request_candidate_registry_prefers_same_kind_before_same_family_fallbacks() {
assert_eq!(
request_candidate_api_formats("openai:chat", false),
vec![
"openai:chat",
"openai:responses",
"claude:messages",
"gemini:generate_content"
]
);
assert_eq!(
request_candidate_api_formats("openai:responses", false),
vec![
"openai:responses",
"openai:chat",
"claude:messages",
"gemini:generate_content"
]
);
assert_eq!(
request_candidate_api_formats("claude:messages", false),
vec![
"claude:messages",
"openai:chat",
"openai:responses",
"gemini:generate_content"
]
);
assert!(
request_candidate_api_format_preference("claude:messages", "openai:chat")
< request_candidate_api_format_preference("claude:messages", "openai:responses")
);
assert_eq!(
request_candidate_api_formats("openai:cli", false),
Vec::<&'static str>::new()
);
assert_eq!(
request_candidate_api_formats("claude:cli", false),
Vec::<&'static str>::new()
);
assert_eq!(
request_candidate_api_formats("openai:compact", false),
Vec::<&'static str>::new()
);
assert_eq!(
request_candidate_api_format_preference("claude:cli", "openai:responses"),
None
);
assert_eq!(
request_candidate_api_format_preference("claude:cli", "claude:chat"),
None
);
assert_eq!(
request_candidate_api_format_preference("claude:cli", "openai:chat"),
None
);
}
#[test]
fn request_conversion_enable_flag_only_applies_to_real_data_format_conversions() {
assert!(!request_conversion_requires_enable_flag(
"claude:messages",
"claude:messages"
));
assert!(request_conversion_requires_enable_flag(
"claude:chat",
"claude:cli"
));
assert!(request_conversion_requires_enable_flag(
"openai:chat",
"openai:responses"
));
assert!(request_conversion_requires_enable_flag(
"claude:messages",
"gemini:generate_content"
));
assert!(request_conversion_requires_enable_flag(
"openai:compact",
"claude:cli"
));
}
}
@@ -0,0 +1,22 @@
pub mod aliyun;
pub mod claude;
pub mod context;
pub mod conversion;
pub mod doubao;
pub mod gemini;
pub mod id;
pub mod jina;
pub mod matrix;
pub mod openai;
pub mod registry;
pub mod shared;
pub use context::{FormatContext, FormatError};
pub use id::{
api_format_alias_matches, api_format_defaults_to_client_error_failover,
api_format_defaults_to_non_stream, api_format_permission_covers,
api_format_permission_storage_aliases, api_format_storage_aliases,
intersect_api_format_allowed_lists, is_openai_responses_compact_format,
is_openai_responses_family_format, is_openai_responses_format, normalize_api_format_alias,
FormatFamily, FormatId, FormatProfile,
};
@@ -0,0 +1,3 @@
pub mod request;
pub mod response;
pub mod stream;
@@ -0,0 +1,417 @@
use serde_json::{json, Map, Value};
use crate::{
formats::context::FormatContext,
protocol::canonical::{
canonical_extension_object_mut, canonical_message_to_openai_chat_messages,
canonical_response_format_to_openai, canonical_tool_choice_to_openai,
canonical_tool_is_openai_custom, canonical_tool_to_openai, is_claude_tool_result,
namespace_extension_object, openai_content_text, openai_extensions,
openai_generation_config, openai_message_content_blocks,
openai_response_format_to_canonical, openai_responses_extension, openai_role_to_canonical,
openai_tool_choice_raw_to_chat, openai_tool_choice_to_canonical, openai_tools_to_canonical,
write_openai_generation_config, CanonicalContentBlock, CanonicalInstruction,
CanonicalRequest, CanonicalRole, CanonicalThinkingConfig, CanonicalToolChoice,
CanonicalToolDefinition, OPENAI_RESPONSES_EXTENSION_NAMESPACE,
OPENAI_RESPONSES_LEGACY_EXTENSION_NAMESPACE,
},
};
pub fn from(body: &Value, _ctx: &FormatContext) -> Option<CanonicalRequest> {
from_raw(body)
}
pub fn to(request: &CanonicalRequest, ctx: &FormatContext) -> Option<Value> {
if canonical_request_has_unrepresentable_claude_tool_result_for_openai_chat(request) {
return None;
}
let mut body = to_raw(request);
force_stream_options(&mut body, ctx.upstream_is_stream);
Some(body)
}
pub fn from_raw(body_json: &Value) -> Option<CanonicalRequest> {
let request = body_json.as_object()?;
let mut canonical = CanonicalRequest {
model: request
.get("model")
.and_then(Value::as_str)
.unwrap_or_default()
.to_string(),
..CanonicalRequest::default()
};
if let Some(messages) = request.get("messages").and_then(Value::as_array) {
for message in messages {
let message_object = message.as_object()?;
let role = openai_role_to_canonical(
message_object
.get("role")
.and_then(Value::as_str)
.unwrap_or_default(),
);
if matches!(role, CanonicalRole::System | CanonicalRole::Developer) {
let text = openai_content_text(message_object.get("content"));
canonical.instructions.push(CanonicalInstruction {
role,
text: text.clone(),
extensions: openai_extensions(message_object, &["role", "content"]),
});
if !text.trim().is_empty() {
canonical.system = Some(match canonical.system.take() {
Some(existing) if !existing.trim().is_empty() => {
format!("{existing}\n\n{text}")
}
_ => text,
});
}
continue;
}
canonical
.messages
.push(crate::protocol::canonical::CanonicalMessage {
role,
content: openai_message_content_blocks(message_object)?,
extensions: openai_extensions(
message_object,
&["role", "content", "tool_calls", "tool_call_id"],
),
});
}
}
canonical.generation = openai_generation_config(request);
canonical.tools = openai_tools_to_canonical(request.get("tools"))?;
canonical.tool_choice = openai_tool_choice_to_canonical(request.get("tool_choice"));
canonical.parallel_tool_calls = request.get("parallel_tool_calls").and_then(Value::as_bool);
canonical.metadata = request.get("metadata").cloned();
canonical.response_format = openai_response_format_to_canonical(request.get("response_format"));
if let Some(reasoning_effort) = request.get("reasoning_effort").and_then(Value::as_str) {
let mut extensions = std::collections::BTreeMap::new();
extensions.insert(
"openai".to_string(),
json!({ "reasoning_effort": reasoning_effort }),
);
canonical.thinking = Some(CanonicalThinkingConfig {
enabled: true,
budget_tokens: None,
extensions,
});
}
canonical.extensions = openai_extensions(
request,
&[
"model",
"messages",
"max_tokens",
"max_completion_tokens",
"temperature",
"top_p",
"top_k",
"stop",
"tools",
"parallel_tool_calls",
"metadata",
"response_format",
"reasoning_effort",
"n",
"presence_penalty",
"frequency_penalty",
"seed",
"logprobs",
"top_logprobs",
],
);
if canonical.tool_choice.is_some() {
remove_tool_choice_extension(&mut canonical.extensions, "openai");
}
if let Some(verbosity) = request.get("verbosity").cloned() {
canonical_extension_object_mut(
&mut canonical.extensions,
OPENAI_RESPONSES_EXTENSION_NAMESPACE,
)
.insert("verbosity".to_string(), verbosity);
}
Some(canonical)
}
pub fn to_raw(canonical: &CanonicalRequest) -> Value {
let mut output = serde_json::Map::new();
if !canonical.model.trim().is_empty() {
output.insert("model".to_string(), Value::String(canonical.model.clone()));
}
let mut messages = Vec::new();
for instruction in &canonical.instructions {
let role = match instruction.role {
CanonicalRole::Developer => "system",
_ => "system",
};
if !instruction.text.trim().is_empty() {
messages.push(json!({
"role": role,
"content": instruction.text,
}));
}
}
for message in &canonical.messages {
messages.extend(canonical_message_to_openai_chat_messages(message));
}
output.insert("messages".to_string(), Value::Array(messages));
write_openai_generation_config(&mut output, &canonical.generation);
if !canonical.tools.is_empty() {
output.insert(
"tools".to_string(),
Value::Array(
canonical
.tools
.iter()
.map(canonical_tool_to_openai)
.collect(),
),
);
}
if let Some(tool_choice) = canonical_tool_choice_to_openai_for_request(canonical) {
output.insert("tool_choice".to_string(), tool_choice);
}
if let Some(value) = canonical.parallel_tool_calls {
output.insert("parallel_tool_calls".to_string(), Value::Bool(value));
}
if let Some(metadata) = canonical.metadata.clone() {
output.insert("metadata".to_string(), metadata);
}
if let Some(response_format) = &canonical.response_format {
output.insert(
"response_format".to_string(),
canonical_response_format_to_openai(response_format),
);
}
if let Some(thinking) = &canonical.thinking {
if let Some(reasoning_effort) = thinking
.extensions
.get("openai")
.and_then(|value| value.get("reasoning_effort"))
.and_then(Value::as_str)
.or_else(|| {
openai_responses_extension(&thinking.extensions)
.and_then(|value| value.get("effort"))
.and_then(Value::as_str)
})
.and_then(openai_chat_reasoning_effort)
{
output.insert(
"reasoning_effort".to_string(),
Value::String(reasoning_effort.to_string()),
);
}
}
output.extend(namespace_extension_object(
&canonical.extensions,
"openai",
&output,
));
output.extend(chat_compatible_openai_responses_extension_object(
&canonical.extensions,
OPENAI_RESPONSES_EXTENSION_NAMESPACE,
&output,
));
output.extend(chat_compatible_openai_responses_extension_object(
&canonical.extensions,
OPENAI_RESPONSES_LEGACY_EXTENSION_NAMESPACE,
&output,
));
Value::Object(output)
}
fn canonical_tool_choice_to_openai_for_request(canonical: &CanonicalRequest) -> Option<Value> {
canonical
.tool_choice
.as_ref()
.map(|tool_choice| canonical_tool_choice_to_openai_for_tools(tool_choice, &canonical.tools))
.or_else(|| raw_tool_choice_extension(canonical).map(openai_tool_choice_raw_to_chat))
}
fn canonical_tool_choice_to_openai_for_tools(
choice: &CanonicalToolChoice,
tools: &[CanonicalToolDefinition],
) -> Value {
match choice {
CanonicalToolChoice::Tool { name }
if tools
.iter()
.any(|tool| tool.name == *name && canonical_tool_is_openai_custom(tool)) =>
{
json!({
"type": "custom",
"custom": { "name": name },
})
}
_ => canonical_tool_choice_to_openai(choice),
}
}
fn raw_tool_choice_extension(canonical: &CanonicalRequest) -> Option<&Value> {
canonical
.extensions
.get("openai")
.and_then(|value| value.get("tool_choice"))
.or_else(|| {
openai_responses_extension(&canonical.extensions)
.and_then(|value| value.get("tool_choice"))
})
}
fn remove_tool_choice_extension(
extensions: &mut std::collections::BTreeMap<String, Value>,
namespace: &str,
) {
let should_remove_namespace = extensions
.get_mut(namespace)
.and_then(Value::as_object_mut)
.is_some_and(|object| {
object.remove("tool_choice");
object.is_empty()
});
if should_remove_namespace {
extensions.remove(namespace);
}
}
fn canonical_request_has_unrepresentable_claude_tool_result_for_openai_chat(
request: &CanonicalRequest,
) -> bool {
request.messages.iter().any(|message| {
message.content.iter().any(|block| {
let CanonicalContentBlock::ToolResult {
output, extensions, ..
} = block
else {
return false;
};
is_claude_tool_result(extensions)
&& output
.as_ref()
.and_then(Value::as_array)
.is_some_and(|parts| {
!claude_tool_result_parts_are_openai_chat_representable(parts)
})
})
})
}
pub(crate) fn claude_tool_result_parts_are_openai_chat_representable(parts: &[Value]) -> bool {
parts
.iter()
.all(claude_tool_result_part_is_openai_chat_representable)
}
fn claude_tool_result_part_is_openai_chat_representable(part: &Value) -> bool {
let Some(part_object) = part.as_object() else {
return false;
};
match part_object
.get("type")
.and_then(Value::as_str)
.unwrap_or_default()
{
"text" => true,
"image" => claude_image_block_is_openai_chat_representable(part_object),
"document" | "file" => claude_document_block_is_openai_chat_representable(part_object),
_ => false,
}
}
fn claude_image_block_is_openai_chat_representable(block: &Map<String, Value>) -> bool {
let Some(source) = block.get("source").and_then(Value::as_object) else {
return false;
};
match source
.get("type")
.and_then(Value::as_str)
.unwrap_or_default()
{
"base64" => {
non_empty_source_str(source, "media_type").is_some()
&& non_empty_source_str(source, "data").is_some()
}
"url" => non_empty_source_str(source, "url").is_some(),
_ => false,
}
}
fn claude_document_block_is_openai_chat_representable(block: &Map<String, Value>) -> bool {
let Some(source) = block.get("source").and_then(Value::as_object) else {
return false;
};
match source
.get("type")
.and_then(Value::as_str)
.unwrap_or_default()
{
"base64" => {
non_empty_source_str(source, "media_type").is_some()
&& non_empty_source_str(source, "data").is_some()
}
"url" => non_empty_source_str(source, "url").is_some(),
"text" => non_empty_source_str(source, "data").is_some(),
_ => false,
}
}
fn non_empty_source_str<'a>(source: &'a Map<String, Value>, key: &str) -> Option<&'a str> {
source
.get(key)
.and_then(Value::as_str)
.filter(|value| !value.trim().is_empty())
}
fn openai_chat_reasoning_effort(value: &str) -> Option<&str> {
(!value.trim().is_empty()).then_some(value)
}
fn chat_compatible_openai_responses_extension_object(
extensions: &std::collections::BTreeMap<String, Value>,
namespace: &str,
existing: &Map<String, Value>,
) -> Map<String, Value> {
namespace_extension_object(extensions, namespace, existing)
.into_iter()
.filter(|(key, _)| {
matches!(
key.as_str(),
"verbosity"
| "store"
| "service_tier"
| "prompt_cache_key"
| "prompt_cache_options"
| "prompt_cache_retention"
| "safety_identifier"
| "user"
)
})
.collect()
}
fn force_stream_options(body: &mut Value, upstream_is_stream: bool) {
if !upstream_is_stream {
return;
}
let Some(object) = body.as_object_mut() else {
return;
};
object.insert("stream".to_string(), Value::Bool(true));
match object.get_mut("stream_options") {
Some(Value::Object(stream_options)) => {
stream_options.insert("include_usage".to_string(), Value::Bool(true));
}
_ => {
object.insert(
"stream_options".to_string(),
json!({
"include_usage": true,
}),
);
}
}
}
@@ -0,0 +1,181 @@
use std::collections::BTreeMap;
use serde_json::{json, Value};
use crate::{
formats::context::FormatContext,
protocol::canonical::{
canonical_blocks_to_openai_chat_message, canonical_stop_reason_to_openai,
canonical_usage_to_openai, openai_extensions, openai_finish_reason_to_canonical,
openai_message_content_blocks, openai_service_tier_extension, openai_usage_to_canonical,
CanonicalContentBlock, CanonicalResponse, CanonicalResponseOutput, CanonicalRole,
OPENAI_RESPONSES_EXTENSION_NAMESPACE, OPENAI_RESPONSES_LEGACY_EXTENSION_NAMESPACE,
},
};
pub fn from(body: &Value, _ctx: &FormatContext) -> Option<CanonicalResponse> {
from_raw(body)
}
pub fn to(response: &CanonicalResponse, _ctx: &FormatContext) -> Option<Value> {
Some(to_raw(response))
}
pub fn from_raw(body_json: &Value) -> Option<CanonicalResponse> {
let body = body_json.as_object()?;
if body.contains_key("error") {
return None;
}
let mut outputs = Vec::new();
for (fallback_index, choice_value) in body
.get("choices")
.and_then(Value::as_array)?
.iter()
.enumerate()
{
let choice = choice_value.as_object()?;
let message = choice.get("message").and_then(Value::as_object)?;
let mut content = openai_message_content_blocks(message)?;
if !content
.iter()
.any(|block| matches!(block, CanonicalContentBlock::Thinking { .. }))
{
if let Some(reasoning_content) = message
.get("reasoning_content")
.and_then(Value::as_str)
.filter(|value| !value.trim().is_empty())
{
content.insert(
0,
CanonicalContentBlock::Thinking {
text: reasoning_content.to_string(),
signature: None,
encrypted_content: None,
extensions: BTreeMap::new(),
},
);
}
}
let stop_reason =
openai_finish_reason_to_canonical(choice.get("finish_reason").and_then(Value::as_str));
let mut extensions = BTreeMap::new();
if let Some(raw_finish_reason) = choice.get("finish_reason").cloned() {
extensions.insert(
"openai".to_string(),
json!({ "raw_finish_reason": raw_finish_reason }),
);
}
outputs.push(CanonicalResponseOutput {
index: choice
.get("index")
.and_then(Value::as_u64)
.map(|value| value as usize)
.unwrap_or(fallback_index),
role: CanonicalRole::Assistant,
content,
stop_reason,
extensions,
});
}
let first_output = outputs.first()?;
let content = first_output.content.clone();
let stop_reason = first_output.stop_reason.clone();
Some(CanonicalResponse {
id: body
.get("id")
.and_then(Value::as_str)
.unwrap_or("chatcmpl-unknown")
.to_string(),
model: body
.get("model")
.and_then(Value::as_str)
.unwrap_or("unknown")
.to_string(),
outputs,
content,
stop_reason,
usage: openai_usage_to_canonical(body.get("usage")),
extensions: openai_extensions(
body,
&["id", "object", "model", "choices", "usage", "created"],
),
})
}
pub fn to_raw(canonical: &CanonicalResponse) -> Value {
let outputs: Vec<CanonicalResponseOutput> = if canonical.outputs.is_empty() {
vec![CanonicalResponseOutput {
index: 0,
role: CanonicalRole::Assistant,
content: canonical.content.clone(),
stop_reason: canonical.stop_reason.clone(),
extensions: BTreeMap::new(),
}]
} else {
canonical.outputs.clone()
};
let choices: Vec<Value> = outputs
.iter()
.enumerate()
.map(|(fallback_index, output)| {
let finish_reason = output
.extensions
.get("openai")
.and_then(Value::as_object)
.and_then(|openai| openai.get("raw_finish_reason"))
.cloned()
.unwrap_or_else(|| {
Value::String(
canonical_stop_reason_to_openai(output.stop_reason.as_ref()).to_string(),
)
});
json!({
"index": output.index,
"message": canonical_blocks_to_openai_chat_message(&output.content),
"finish_reason": finish_reason,
})
.as_object()
.map(|choice| {
let mut choice = choice.clone();
if output.index == 0 && fallback_index != 0 {
choice.insert("index".to_string(), Value::from(fallback_index as u64));
}
Value::Object(choice)
})
.unwrap_or_else(|| json!({}))
})
.collect();
let mut response = json!({
"id": canonical.id,
"object": "chat.completion",
"model": canonical.model,
"choices": choices,
"usage": canonical.usage.as_ref().map(canonical_usage_to_openai).unwrap_or_else(|| json!({
"prompt_tokens": 0,
"completion_tokens": 0,
"total_tokens": 0,
})),
});
if let Some(created_at) = canonical
.extensions
.get(OPENAI_RESPONSES_EXTENSION_NAMESPACE)
.or_else(|| {
canonical
.extensions
.get(OPENAI_RESPONSES_LEGACY_EXTENSION_NAMESPACE)
})
.and_then(|value| value.get("created_at"))
.and_then(|value| {
value
.as_i64()
.or_else(|| value.as_u64().map(|value| value as i64))
})
{
response["created"] = Value::from(created_at);
}
if let Some(service_tier) = openai_service_tier_extension(&canonical.extensions).cloned() {
response["service_tier"] = service_tier;
}
response
}
File diff suppressed because it is too large Load Diff
@@ -0,0 +1,3 @@
pub mod request;
pub mod response;
pub mod spec;
@@ -0,0 +1,161 @@
use serde_json::Map;
use serde_json::Value;
use std::collections::BTreeMap;
use crate::formats::context::FormatContext;
use crate::protocol::canonical::{
namespace_extension_object, CanonicalEmbeddingInput, CanonicalEmbeddingRequest,
CanonicalRequest,
};
pub fn from(body: &Value, _ctx: &FormatContext) -> Option<CanonicalRequest> {
from_namespace(body, "openai")
}
pub fn to(request: &CanonicalRequest, ctx: &FormatContext) -> Option<Value> {
to_openai_like(
request,
ctx.mapped_model_or(request.model.as_str()),
"openai",
false,
)
}
pub(crate) fn from_namespace(body_json: &Value, namespace: &str) -> Option<CanonicalRequest> {
let request = body_json.as_object()?;
let model = request
.get("model")
.and_then(Value::as_str)
.map(str::trim)
.filter(|value| !value.is_empty())?
.to_string();
let input =
serde_json::from_value::<CanonicalEmbeddingInput>(request.get("input")?.clone()).ok()?;
if input.is_empty() {
return None;
}
let embedding = CanonicalEmbeddingRequest {
input,
encoding_format: request
.get("encoding_format")
.and_then(Value::as_str)
.map(ToOwned::to_owned),
dimensions: request.get("dimensions").and_then(Value::as_u64),
task: request
.get("task")
.and_then(Value::as_str)
.map(ToOwned::to_owned),
user: request
.get("user")
.and_then(Value::as_str)
.map(ToOwned::to_owned),
parameters: request
.get("parameters")
.and_then(Value::as_object)
.cloned(),
extensions: namespace_extensions(
namespace,
request,
&[
"model",
"input",
"encoding_format",
"dimensions",
"task",
"user",
"parameters",
],
),
};
Some(CanonicalRequest {
model,
embedding: Some(embedding),
..CanonicalRequest::default()
})
}
pub(crate) fn to_openai_like(
canonical: &CanonicalRequest,
mapped_model: &str,
namespace: &str,
default_task: bool,
) -> Option<Value> {
let embedding = canonical.embedding.as_ref()?;
if embedding.input.is_empty() {
return None;
}
if matches!(&embedding.input, CanonicalEmbeddingInput::Multimodal(_)) {
return None;
}
let mut output = Map::new();
output.insert(
"model".to_string(),
Value::String(mapped_embedding_model(canonical, mapped_model)),
);
output.insert(
"input".to_string(),
serde_json::to_value(&embedding.input).ok()?,
);
if let Some(value) = &embedding.encoding_format {
output.insert("encoding_format".to_string(), Value::String(value.clone()));
}
if let Some(value) = embedding.dimensions {
output.insert("dimensions".to_string(), Value::from(value));
}
if let Some(value) = &embedding.user {
output.insert("user".to_string(), Value::String(value.clone()));
}
if let Some(value) = &embedding.parameters {
output.insert("parameters".to_string(), Value::Object(value.clone()));
}
if let Some(task) = embedding
.task
.as_ref()
.filter(|value| !value.trim().is_empty())
{
output.insert("task".to_string(), Value::String(task.clone()));
} else if default_task {
output.insert(
"task".to_string(),
Value::String("text-matching".to_string()),
);
}
output.extend(namespace_extension_object(
&embedding.extensions,
namespace,
&output,
));
Some(Value::Object(output))
}
pub(crate) fn mapped_embedding_model(canonical: &CanonicalRequest, mapped_model: &str) -> String {
let mapped_model = mapped_model.trim();
if mapped_model.is_empty() {
canonical.model.clone()
} else {
mapped_model.to_string()
}
}
pub(crate) fn namespace_extensions(
namespace: &str,
object: &Map<String, Value>,
handled_keys: &[&str],
) -> BTreeMap<String, Value> {
let handled = handled_keys
.iter()
.copied()
.collect::<std::collections::BTreeSet<_>>();
let raw = object
.iter()
.filter(|(key, _)| !handled.contains(key.as_str()))
.map(|(key, value)| (key.clone(), value.clone()))
.collect::<Map<String, Value>>();
if raw.is_empty() {
BTreeMap::new()
} else {
BTreeMap::from([(namespace.to_string(), Value::Object(raw))])
}
}
@@ -0,0 +1,106 @@
use serde_json::Value;
use serde_json::{json, Map};
use crate::formats::openai::embedding::request::namespace_extensions;
use crate::protocol::canonical::{
canonical_usage_to_openai, namespace_extension_object, openai_usage_to_canonical,
CanonicalEmbedding, CanonicalEmbeddingResponse,
};
pub fn from(body: &Value) -> Option<CanonicalEmbeddingResponse> {
from_namespace(body, "openai")
}
pub fn to(response: &CanonicalEmbeddingResponse) -> Option<Value> {
Some(to_openai_like(response, "openai"))
}
pub(crate) fn from_namespace(
body_json: &Value,
namespace: &str,
) -> Option<CanonicalEmbeddingResponse> {
let body = body_json.as_object()?;
if body.contains_key("error") {
return None;
}
let data = body.get("data")?.as_array()?;
let mut embeddings = Vec::new();
for (fallback_index, item) in data.iter().enumerate() {
let item_object = item.as_object()?;
let values = item_object.get("embedding")?.as_array()?;
let embedding = values
.iter()
.map(Value::as_f64)
.collect::<Option<Vec<_>>>()?;
embeddings.push(CanonicalEmbedding {
index: item_object
.get("index")
.and_then(Value::as_u64)
.and_then(|value| usize::try_from(value).ok())
.unwrap_or(fallback_index),
embedding,
extensions: namespace_extensions(
namespace,
item_object,
&["object", "index", "embedding"],
),
});
}
Some(CanonicalEmbeddingResponse {
id: body
.get("id")
.and_then(Value::as_str)
.unwrap_or("embd-unknown")
.to_string(),
model: body
.get("model")
.and_then(Value::as_str)
.unwrap_or("unknown")
.to_string(),
embeddings,
usage: openai_usage_to_canonical(body.get("usage")),
extensions: namespace_extensions(
namespace,
body,
&["id", "object", "model", "data", "usage"],
),
})
}
pub(crate) fn to_openai_like(canonical: &CanonicalEmbeddingResponse, namespace: &str) -> Value {
let mut response = Map::new();
response.insert("object".to_string(), Value::String("list".to_string()));
if !canonical.model.trim().is_empty() && canonical.model != "unknown" {
response.insert("model".to_string(), Value::String(canonical.model.clone()));
}
response.insert(
"data".to_string(),
Value::Array(
canonical
.embeddings
.iter()
.map(|embedding| {
let mut item = Map::new();
item.insert("object".to_string(), Value::String("embedding".to_string()));
item.insert("index".to_string(), Value::from(embedding.index as u64));
item.insert("embedding".to_string(), json!(embedding.embedding));
item.extend(namespace_extension_object(
&embedding.extensions,
namespace,
&item,
));
Value::Object(item)
})
.collect(),
),
);
if let Some(usage) = &canonical.usage {
response.insert("usage".to_string(), canonical_usage_to_openai(usage));
}
response.extend(namespace_extension_object(
&canonical.extensions,
namespace,
&response,
));
Value::Object(response)
}
@@ -0,0 +1,35 @@
use crate::contracts::{
OPENAI_EMBEDDING_SYNC_FINALIZE_REPORT_KIND, OPENAI_EMBEDDING_SYNC_PLAN_KIND,
};
use crate::formats::shared::family::{
LocalStandardSourceFamily, LocalStandardSourceMode, LocalStandardSpec,
};
pub fn resolve_sync_spec(plan_kind: &str) -> Option<LocalStandardSpec> {
match plan_kind {
OPENAI_EMBEDDING_SYNC_PLAN_KIND => Some(LocalStandardSpec {
api_format: "openai:embedding",
decision_kind: OPENAI_EMBEDDING_SYNC_PLAN_KIND,
report_kind: OPENAI_EMBEDDING_SYNC_FINALIZE_REPORT_KIND,
family: LocalStandardSourceFamily::Standard,
mode: LocalStandardSourceMode::Embedding,
require_streaming: false,
}),
_ => None,
}
}
#[cfg(test)]
mod tests {
use super::resolve_sync_spec;
use crate::formats::shared::family::LocalStandardSourceMode;
#[test]
fn resolves_openai_embedding_sync_standard_spec() {
let spec = resolve_sync_spec("openai_embedding_sync").expect("spec");
assert_eq!(spec.api_format, "openai:embedding");
assert_eq!(spec.report_kind, "openai_embedding_sync_finalize");
assert_eq!(spec.mode, LocalStandardSourceMode::Embedding);
assert!(!spec.require_streaming);
}
}
@@ -0,0 +1,3 @@
pub mod request;
pub mod spec;
pub mod stream;
File diff suppressed because it is too large Load Diff
@@ -0,0 +1,54 @@
use crate::contracts::{OPENAI_IMAGE_STREAM_PLAN_KIND, OPENAI_IMAGE_SYNC_PLAN_KIND};
#[derive(Debug, Clone, Copy)]
pub struct LocalOpenAiImageSpec {
pub api_format: &'static str,
pub decision_kind: &'static str,
pub report_kind: &'static str,
pub require_streaming: bool,
}
pub fn resolve_sync_spec(plan_kind: &str) -> Option<LocalOpenAiImageSpec> {
match plan_kind {
OPENAI_IMAGE_SYNC_PLAN_KIND => Some(LocalOpenAiImageSpec {
api_format: "openai:image",
decision_kind: OPENAI_IMAGE_SYNC_PLAN_KIND,
report_kind: "openai_image_sync_finalize",
require_streaming: false,
}),
_ => None,
}
}
pub fn resolve_stream_spec(plan_kind: &str) -> Option<LocalOpenAiImageSpec> {
match plan_kind {
OPENAI_IMAGE_STREAM_PLAN_KIND => Some(LocalOpenAiImageSpec {
api_format: "openai:image",
decision_kind: OPENAI_IMAGE_STREAM_PLAN_KIND,
report_kind: "openai_image_stream_success",
require_streaming: true,
}),
_ => None,
}
}
#[cfg(test)]
mod tests {
use super::{resolve_stream_spec, resolve_sync_spec};
#[test]
fn resolves_openai_image_sync_spec() {
let spec = resolve_sync_spec("openai_image_sync").expect("spec");
assert_eq!(spec.api_format, "openai:image");
assert_eq!(spec.report_kind, "openai_image_sync_finalize");
assert!(!spec.require_streaming);
}
#[test]
fn resolves_openai_image_stream_spec() {
let spec = resolve_stream_spec("openai_image_stream").expect("spec");
assert_eq!(spec.api_format, "openai:image");
assert_eq!(spec.report_kind, "openai_image_stream_success");
assert!(spec.require_streaming);
}
}
File diff suppressed because it is too large Load Diff
@@ -0,0 +1,11 @@
pub mod chat;
pub mod embedding;
pub mod image;
pub mod prompt_cache;
pub mod reasoning;
pub mod request_contract;
pub mod rerank;
pub mod responses;
pub mod search;
pub mod shared;
pub mod video;
@@ -0,0 +1,663 @@
use serde_json::Value;
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum OpenAiPromptCacheViolationKind {
InvalidType,
InvalidEnum,
UnsupportedForModel,
UnsupportedContentBlock,
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct OpenAiPromptCacheContractViolation {
pub kind: OpenAiPromptCacheViolationKind,
pub field: String,
pub value: Option<String>,
pub reason: String,
}
pub fn validate_openai_prompt_cache_request(
source_api_format: &str,
provider_model: &str,
body: &Value,
) -> Result<(), OpenAiPromptCacheContractViolation> {
let source_model = body
.get("model")
.and_then(Value::as_str)
.unwrap_or_default();
validate_openai_prompt_cache_request_with_source_model(
source_api_format,
provider_model,
source_model,
body,
)
}
pub fn resolve_openai_prompt_cache_ttl_minutes(
provider_api_format: &str,
provider_model: &str,
source_model: &str,
body: &Value,
) -> Option<i64> {
OpenAiPromptCacheApi::parse(provider_api_format)?;
let request = body.as_object()?;
let explicit_ttl = request
.get("prompt_cache_options")
.and_then(Value::as_object)
.and_then(|options| options.get("ttl"))
.and_then(Value::as_str);
if explicit_ttl == Some("30m") {
return Some(30);
}
let capability_model =
crate::formats::shared::model_directives::openai_model_capability_identity(
provider_model,
source_model,
);
crate::openai_model_supports_prompt_cache_options(&capability_model).then_some(30)
}
pub(crate) fn validate_openai_prompt_cache_request_with_source_model(
source_api_format: &str,
provider_model: &str,
source_model: &str,
body: &Value,
) -> Result<(), OpenAiPromptCacheContractViolation> {
let Some(api) = OpenAiPromptCacheApi::parse(source_api_format) else {
return Ok(());
};
let Some(request) = body.as_object() else {
return Ok(());
};
let capability_model =
crate::formats::shared::model_directives::openai_model_capability_identity(
provider_model,
source_model,
);
let supports_prompt_cache_options =
crate::formats::shared::model_directives::openai_model_capability_is_opaque(
provider_model,
source_model,
) || crate::openai_model_supports_prompt_cache_options(&capability_model);
if let Some(options) = request
.get("prompt_cache_options")
.filter(|value| !value.is_null())
{
validate_prompt_cache_options(options, supports_prompt_cache_options)?;
}
if let Some(retention) = request
.get("prompt_cache_retention")
.filter(|value| !value.is_null())
{
validate_prompt_cache_retention(retention, &capability_model)?;
}
match api {
OpenAiPromptCacheApi::Chat => {
validate_chat_prompt_cache_breakpoints(request, supports_prompt_cache_options)
}
OpenAiPromptCacheApi::Responses => {
validate_responses_prompt_cache_breakpoints(request, supports_prompt_cache_options)
}
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
enum OpenAiPromptCacheApi {
Chat,
Responses,
}
impl OpenAiPromptCacheApi {
fn parse(api_format: &str) -> Option<Self> {
match crate::normalize_api_format_alias(api_format).as_str() {
"openai:chat" => Some(Self::Chat),
"openai:responses" | "openai:responses:compact" => Some(Self::Responses),
_ => None,
}
}
}
fn normalize_provider_model(model: &str) -> &str {
model.trim().rsplit('/').next().unwrap_or_default()
}
fn validate_prompt_cache_options(
value: &Value,
supported_for_model: bool,
) -> Result<(), OpenAiPromptCacheContractViolation> {
if !supported_for_model {
return Err(unsupported_for_model(
"prompt_cache_options",
"provider model does not support prompt_cache_options",
));
}
let Some(options) = value.as_object() else {
return Err(invalid_type(
"prompt_cache_options",
value,
"prompt_cache_options must be an object",
));
};
if let Some(mode) = options.get("mode") {
let Some(raw) = mode.as_str() else {
return Err(invalid_type(
"prompt_cache_options.mode",
mode,
"prompt_cache_options.mode must be a string",
));
};
if !matches!(raw, "implicit" | "explicit") {
return Err(invalid_enum(
"prompt_cache_options.mode",
raw,
"prompt_cache_options.mode supports implicit or explicit",
));
}
}
if let Some(ttl) = options.get("ttl") {
let Some(raw) = ttl.as_str() else {
return Err(invalid_type(
"prompt_cache_options.ttl",
ttl,
"prompt_cache_options.ttl must be a string",
));
};
if raw != "30m" {
return Err(invalid_enum(
"prompt_cache_options.ttl",
raw,
"prompt_cache_options.ttl supports 30m",
));
}
}
Ok(())
}
fn validate_prompt_cache_retention(
value: &Value,
provider_model: &str,
) -> Result<(), OpenAiPromptCacheContractViolation> {
if crate::openai_model_supports_prompt_cache_options(provider_model) {
return Err(unsupported_for_model(
"prompt_cache_retention",
"provider model uses prompt_cache_options.ttl",
));
}
let Some(raw) = value.as_str() else {
return Err(invalid_type(
"prompt_cache_retention",
value,
"prompt_cache_retention must be a string",
));
};
if super::shared::OpenAiPromptCacheRetention::parse(raw).is_none() {
return Err(invalid_enum(
"prompt_cache_retention",
raw,
"prompt_cache_retention supports in_memory or 24h",
));
}
if gpt_5_5_retention_is_24h_only(provider_model) && raw != "24h" {
return Err(invalid_enum(
"prompt_cache_retention",
raw,
"GPT-5.5 family models support 24h retention",
));
}
Ok(())
}
fn gpt_5_5_retention_is_24h_only(model: &str) -> bool {
let normalized = normalize_provider_model(model)
.to_ascii_lowercase()
.replace('_', "-");
matches!(normalized.as_str(), "gpt-5.5" | "gpt-5.5-pro")
}
fn validate_chat_prompt_cache_breakpoints(
request: &serde_json::Map<String, Value>,
supported_for_model: bool,
) -> Result<(), OpenAiPromptCacheContractViolation> {
let Some(messages) = request.get("messages").and_then(Value::as_array) else {
return Ok(());
};
for (message_index, message) in messages.iter().enumerate() {
let Some(content) = message
.as_object()
.and_then(|message| message.get("content"))
.and_then(Value::as_array)
else {
continue;
};
for (content_index, part) in content.iter().enumerate() {
let Some(part) = part.as_object() else {
continue;
};
let Some(breakpoint) = part.get("prompt_cache_breakpoint") else {
continue;
};
let block_type = part.get("type").and_then(Value::as_str);
let supported = matches!(
block_type,
Some("text" | "image_url" | "input_audio" | "file" | "refusal")
);
validate_prompt_cache_breakpoint(
breakpoint,
&format!(
"messages[{message_index}].content[{content_index}].prompt_cache_breakpoint"
),
supported,
supported_for_model,
)?;
}
}
Ok(())
}
fn validate_responses_prompt_cache_breakpoints(
request: &serde_json::Map<String, Value>,
supported_for_model: bool,
) -> Result<(), OpenAiPromptCacheContractViolation> {
let Some(input) = request.get("input").and_then(Value::as_array) else {
return Ok(());
};
for (item_index, item) in input.iter().enumerate() {
let Some(item) = item.as_object() else {
continue;
};
if let Some(breakpoint) = item.get("prompt_cache_breakpoint") {
validate_prompt_cache_breakpoint(
breakpoint,
&format!("input[{item_index}].prompt_cache_breakpoint"),
responses_cache_breakpoint_block_is_supported(item),
supported_for_model,
)?;
}
let Some(content) = item.get("content").and_then(Value::as_array) else {
continue;
};
for (content_index, part) in content.iter().enumerate() {
let Some(part) = part.as_object() else {
continue;
};
let Some(breakpoint) = part.get("prompt_cache_breakpoint") else {
continue;
};
validate_prompt_cache_breakpoint(
breakpoint,
&format!("input[{item_index}].content[{content_index}].prompt_cache_breakpoint"),
responses_cache_breakpoint_block_is_supported(part),
supported_for_model,
)?;
}
}
Ok(())
}
fn responses_cache_breakpoint_block_is_supported(block: &serde_json::Map<String, Value>) -> bool {
matches!(
block.get("type").and_then(Value::as_str),
Some("input_text" | "input_image" | "input_file")
)
}
fn validate_prompt_cache_breakpoint(
value: &Value,
field: &str,
supported_content_block: bool,
supported_for_model: bool,
) -> Result<(), OpenAiPromptCacheContractViolation> {
if !supported_for_model {
return Err(unsupported_for_model(
field,
"provider model does not support prompt_cache_breakpoint",
));
}
if !supported_content_block {
return Err(OpenAiPromptCacheContractViolation {
kind: OpenAiPromptCacheViolationKind::UnsupportedContentBlock,
field: field.to_string(),
value: None,
reason: "content block does not support prompt_cache_breakpoint".to_string(),
});
}
let Some(breakpoint) = value.as_object() else {
return Err(invalid_type(
field,
value,
"prompt_cache_breakpoint must be an object",
));
};
let mode_field = format!("{field}.mode");
let Some(mode) = breakpoint.get("mode") else {
return Err(OpenAiPromptCacheContractViolation {
kind: OpenAiPromptCacheViolationKind::InvalidType,
field: mode_field,
value: None,
reason: "prompt_cache_breakpoint.mode is required".to_string(),
});
};
let Some(raw) = mode.as_str() else {
return Err(invalid_type(
&mode_field,
mode,
"prompt_cache_breakpoint.mode must be a string",
));
};
if raw != "explicit" {
return Err(invalid_enum(
&mode_field,
raw,
"prompt_cache_breakpoint.mode supports explicit",
));
}
Ok(())
}
fn invalid_type(field: &str, value: &Value, reason: &str) -> OpenAiPromptCacheContractViolation {
OpenAiPromptCacheContractViolation {
kind: OpenAiPromptCacheViolationKind::InvalidType,
field: field.to_string(),
value: Some(value.to_string()),
reason: reason.to_string(),
}
}
fn invalid_enum(field: &str, value: &str, reason: &str) -> OpenAiPromptCacheContractViolation {
OpenAiPromptCacheContractViolation {
kind: OpenAiPromptCacheViolationKind::InvalidEnum,
field: field.to_string(),
value: Some(value.to_string()),
reason: reason.to_string(),
}
}
fn unsupported_for_model(field: &str, reason: &str) -> OpenAiPromptCacheContractViolation {
OpenAiPromptCacheContractViolation {
kind: OpenAiPromptCacheViolationKind::UnsupportedForModel,
field: field.to_string(),
value: None,
reason: reason.to_string(),
}
}
#[cfg(test)]
mod tests {
use serde_json::json;
use super::{
resolve_openai_prompt_cache_ttl_minutes, validate_openai_prompt_cache_request,
OpenAiPromptCacheViolationKind,
};
#[test]
fn resolves_effective_prompt_cache_ttl_from_current_openai_contract() {
for body in [
json!({"model": "client-alias"}),
json!({
"model": "client-alias",
"prompt_cache_options": {"mode": "implicit"}
}),
json!({
"model": "client-alias",
"prompt_cache_options": {"mode": "explicit", "ttl": "30m"}
}),
] {
assert_eq!(
resolve_openai_prompt_cache_ttl_minutes(
"openai:responses",
"gpt-5.6-sol",
"client-alias",
&body,
),
Some(30)
);
}
assert_eq!(
resolve_openai_prompt_cache_ttl_minutes(
"openai:chat",
"deployment-alias",
"gpt-5.6-terra",
&json!({"model": "gpt-5.6-terra"}),
),
Some(30)
);
assert_eq!(
resolve_openai_prompt_cache_ttl_minutes(
"openai:chat",
"gpt-5.5",
"gpt-5.6-terra",
&json!({"model": "gpt-5.6-terra"}),
),
None
);
assert_eq!(
resolve_openai_prompt_cache_ttl_minutes(
"claude:messages",
"gpt-5.6-sol",
"gpt-5.6-sol",
&json!({"model": "gpt-5.6-sol"}),
),
None
);
}
#[test]
fn gpt_5_6_accepts_current_prompt_cache_options_and_breakpoints() {
for (format, body) in [
(
"openai:chat",
json!({
"model": "client-alias",
"prompt_cache_options": {"mode": "implicit", "ttl": "30m"},
"messages": [{
"role": "user",
"content": [{
"type": "input_audio",
"input_audio": {"data": "ZmFrZQ==", "format": "mp3"},
"prompt_cache_breakpoint": {"mode": "explicit"}
}]
}]
}),
),
(
"openai:responses",
json!({
"model": "client-alias",
"prompt_cache_options": {"mode": "explicit", "ttl": "30m"},
"input": [{
"type": "message",
"role": "user",
"content": [{
"type": "input_file",
"file_id": "file_123",
"prompt_cache_breakpoint": {"mode": "explicit"}
}]
}]
}),
),
] {
validate_openai_prompt_cache_request(format, "gpt-5.6-sol", &body)
.expect("GPT-5.6 prompt cache contract should be accepted");
}
}
#[test]
fn gpt_5_6_uses_prompt_cache_options_and_rejects_invalid_enums() {
let retention_error = validate_openai_prompt_cache_request(
"openai:chat",
"gpt-5.6-sol",
&json!({"prompt_cache_retention": "24h"}),
)
.expect_err("GPT-5.6 uses prompt_cache_options.ttl");
assert_eq!(
retention_error.kind,
OpenAiPromptCacheViolationKind::UnsupportedForModel
);
let cases = [
(
json!({"prompt_cache_options": {"mode": "automatic"}}),
"prompt_cache_options.mode",
OpenAiPromptCacheViolationKind::InvalidEnum,
),
(
json!({"prompt_cache_options": {"ttl": "1h"}}),
"prompt_cache_options.ttl",
OpenAiPromptCacheViolationKind::InvalidEnum,
),
(
json!({
"messages": [{
"role": "user",
"content": [{
"type": "text",
"text": "stable",
"prompt_cache_breakpoint": {"mode": "implicit"}
}]
}]
}),
"messages[0].content[0].prompt_cache_breakpoint.mode",
OpenAiPromptCacheViolationKind::InvalidEnum,
),
];
for (body, field, kind) in cases {
let error = validate_openai_prompt_cache_request("openai:chat", "gpt-5.6-sol", &body)
.expect_err("invalid GPT-5.6 cache contract should fail");
assert_eq!(error.field, field);
assert_eq!(error.kind, kind);
}
}
#[test]
fn prompt_cache_capability_uses_the_provider_model() {
let options = json!({
"model": "gpt-5.5",
"prompt_cache_options": {"mode": "explicit", "ttl": "30m"},
"messages": [{"role": "user", "content": "hello"}]
});
validate_openai_prompt_cache_request("openai:chat", "gpt-5.6-terra", &options)
.expect("mapped GPT-5.6 provider model should enable prompt_cache_options");
let error = validate_openai_prompt_cache_request("openai:chat", "gpt-5.5", &options)
.expect_err("mapped earlier provider model should reject prompt_cache_options");
assert_eq!(
error.kind,
OpenAiPromptCacheViolationKind::UnsupportedForModel
);
let error =
validate_openai_prompt_cache_request("openai:chat", "deployment-alias", &options)
.expect_err("opaque provider model must not inherit source model capability");
assert_eq!(
error.kind,
OpenAiPromptCacheViolationKind::UnsupportedForModel
);
let retention = json!({
"model": "gpt-5.6-sol",
"prompt_cache_retention": "24h",
"messages": [{"role": "user", "content": "hello"}]
});
validate_openai_prompt_cache_request("openai:chat", "gpt-5.5", &retention)
.expect("mapped earlier provider model should retain its retention contract");
let error = validate_openai_prompt_cache_request("openai:chat", "gpt-5.6-luna", &retention)
.expect_err("GPT-5.6 uses prompt_cache_options.ttl");
assert_eq!(
error.kind,
OpenAiPromptCacheViolationKind::UnsupportedForModel
);
}
#[test]
fn prompt_cache_breakpoints_validate_supported_blocks_per_api() {
let cases = [
(
"openai:chat",
json!({
"messages": [{
"role": "user",
"content": [{
"type": "video_url",
"video_url": {"url": "https://example.com/video.mp4"},
"prompt_cache_breakpoint": {"mode": "explicit"}
}]
}]
}),
"messages[0].content[0].prompt_cache_breakpoint",
),
(
"openai:responses",
json!({
"input": [{
"type": "message",
"role": "user",
"content": [{
"type": "input_audio",
"input_audio": {"data": "ZmFrZQ==", "format": "mp3"},
"prompt_cache_breakpoint": {"mode": "explicit"}
}]
}]
}),
"input[0].content[0].prompt_cache_breakpoint",
),
];
for (format, body, field) in cases {
let error = validate_openai_prompt_cache_request(format, "gpt-5.6-sol", &body)
.expect_err("unsupported cache breakpoint block should fail");
assert_eq!(error.field, field);
assert_eq!(
error.kind,
OpenAiPromptCacheViolationKind::UnsupportedContentBlock
);
}
}
#[test]
fn earlier_models_reject_breakpoint_options_and_validate_retention() {
let options = json!({"prompt_cache_options": {"mode": "explicit"}});
let error = validate_openai_prompt_cache_request("openai:responses", "gpt-5.5", &options)
.expect_err("earlier model should reject prompt_cache_options");
assert_eq!(
error.kind,
OpenAiPromptCacheViolationKind::UnsupportedForModel
);
validate_openai_prompt_cache_request(
"openai:responses",
"gpt-5.5-pro",
&json!({"prompt_cache_retention": "24h"}),
)
.expect("GPT-5.5 supports 24h retention");
let error = validate_openai_prompt_cache_request(
"openai:responses",
"gpt-5.5",
&json!({"prompt_cache_retention": "in_memory"}),
)
.expect_err("GPT-5.5 only supports 24h retention");
assert_eq!(error.kind, OpenAiPromptCacheViolationKind::InvalidEnum);
}
#[test]
fn nullable_prompt_cache_fields_are_treated_as_unconfigured() {
for format in [
"openai:chat",
"openai:responses",
"openai:responses:compact",
] {
validate_openai_prompt_cache_request(
format,
"gpt-5.6-sol",
&json!({
"model": "gpt-5.6-sol",
"prompt_cache_options": null,
"prompt_cache_retention": null
}),
)
.expect("nullable prompt cache fields should be omitted semantically");
}
}
}
@@ -0,0 +1,505 @@
use serde_json::Value;
use crate::formats::shared::model_directives::ReasoningEffort;
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum OpenAiReasoningViolationKind {
InvalidType,
InvalidEnum,
UnsupportedForModel,
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct OpenAiReasoningContractViolation {
pub kind: OpenAiReasoningViolationKind,
pub field: String,
pub value: Option<String>,
pub reason: String,
}
pub fn validate_openai_reasoning_request(
source_api_format: &str,
provider_api_format: &str,
provider_model: &str,
body: &Value,
) -> Result<(), OpenAiReasoningContractViolation> {
let source_model = body
.get("model")
.and_then(Value::as_str)
.unwrap_or_default();
validate_openai_reasoning_request_with_source_model(
source_api_format,
provider_api_format,
provider_model,
source_model,
body,
)
}
pub(crate) fn validate_openai_reasoning_request_with_source_model(
source_api_format: &str,
provider_api_format: &str,
provider_model: &str,
source_model: &str,
body: &Value,
) -> Result<(), OpenAiReasoningContractViolation> {
validate_openai_reasoning_request_with_model_profile(
source_api_format,
provider_api_format,
provider_model,
source_model,
body,
None,
None,
)
}
pub(crate) fn validate_openai_reasoning_request_with_model_profile(
source_api_format: &str,
provider_api_format: &str,
provider_model: &str,
source_model: &str,
body: &Value,
model_card_reasoning_efforts: Option<&[String]>,
supports_reasoning_mode: Option<bool>,
) -> Result<(), OpenAiReasoningContractViolation> {
let Some(object) = body.as_object() else {
return Ok(());
};
let source_api_format = crate::normalize_api_format_alias(source_api_format);
let reasoning = match source_api_format.as_str() {
"openai:responses" | "openai:responses:compact" | "openai:search" => {
match object.get("reasoning") {
Some(Value::Object(reasoning)) => Some(reasoning),
Some(Value::Null) => None,
Some(value) => {
return Err(OpenAiReasoningContractViolation {
kind: OpenAiReasoningViolationKind::InvalidType,
field: "reasoning".to_string(),
value: Some(value.to_string()),
reason: "reasoning must be an object".to_string(),
});
}
None => None,
}
}
"openai:chat" => None,
_ => return Ok(()),
};
let provider_model = provider_model.trim();
let provider_model = if provider_model.is_empty() {
source_model
} else {
provider_model
};
let effort = match source_api_format.as_str() {
"openai:chat" => object.get("reasoning_effort"),
"openai:responses" | "openai:responses:compact" | "openai:search" => {
reasoning.and_then(|reasoning| reasoning.get("effort"))
}
_ => None,
};
if let Some(value) = effort.filter(|value| !value.is_null()) {
validate_reasoning_effort(
value,
source_api_format.as_str(),
provider_api_format,
provider_model,
source_model,
model_card_reasoning_efforts,
)?;
}
if source_api_format != "openai:search" {
if let Some(mode) = reasoning
.and_then(|reasoning| reasoning.get("mode"))
.filter(|value| !value.is_null())
{
validate_reasoning_mode(mode, provider_model, source_model, supports_reasoning_mode)?;
}
}
if let Some(context) = reasoning
.and_then(|reasoning| reasoning.get("context"))
.filter(|value| !value.is_null())
{
validate_reasoning_context(context)?;
}
if let Some(summary) = reasoning
.and_then(|reasoning| reasoning.get("summary"))
.filter(|value| !value.is_null())
{
validate_reasoning_summary(summary)?;
}
Ok(())
}
fn validate_reasoning_effort(
value: &Value,
source_api_format: &str,
provider_api_format: &str,
provider_model: &str,
source_model: &str,
model_card_reasoning_efforts: Option<&[String]>,
) -> Result<(), OpenAiReasoningContractViolation> {
let field = if source_api_format == "openai:chat" {
"reasoning_effort"
} else {
"reasoning.effort"
};
let Some(raw) = value.as_str() else {
return Err(OpenAiReasoningContractViolation {
kind: OpenAiReasoningViolationKind::InvalidType,
field: field.to_string(),
value: Some(value.to_string()),
reason: "reasoning effort must be a string".to_string(),
});
};
if raw.trim().is_empty() {
return Err(OpenAiReasoningContractViolation {
kind: OpenAiReasoningViolationKind::InvalidEnum,
field: field.to_string(),
value: Some(raw.to_string()),
reason: "reasoning effort must not be empty".to_string(),
});
}
if raw.trim() == "ultra" {
return Err(OpenAiReasoningContractViolation {
kind: OpenAiReasoningViolationKind::InvalidEnum,
field: field.to_string(),
value: Some(raw.to_string()),
reason: "ultra is a Codex client preset, not an OpenAI wire effort".to_string(),
});
}
if let Some(supported_efforts) =
model_card_reasoning_efforts.filter(|values| !values.is_empty())
{
if supported_efforts
.iter()
.any(|effort| effort == raw.trim() || (raw.trim() == "max" && effort == "ultra"))
{
return Ok(());
}
return Err(OpenAiReasoningContractViolation {
kind: OpenAiReasoningViolationKind::UnsupportedForModel,
field: field.to_string(),
value: Some(raw.to_string()),
reason: "provider model card does not support the requested reasoning effort"
.to_string(),
});
}
let Some(effort) = ReasoningEffort::parse(raw) else {
return Ok(());
};
if crate::reasoning_effort_supported_for_model(
provider_api_format,
provider_model,
source_model,
effort,
) {
return Ok(());
}
Err(OpenAiReasoningContractViolation {
kind: OpenAiReasoningViolationKind::UnsupportedForModel,
field: field.to_string(),
value: Some(raw.to_string()),
reason: "provider model does not support the requested reasoning effort".to_string(),
})
}
fn validate_reasoning_mode(
value: &Value,
provider_model: &str,
source_model: &str,
supports_reasoning_mode: Option<bool>,
) -> Result<(), OpenAiReasoningContractViolation> {
let Some(mode) = value.as_str() else {
return Err(OpenAiReasoningContractViolation {
kind: OpenAiReasoningViolationKind::InvalidType,
field: "reasoning.mode".to_string(),
value: Some(value.to_string()),
reason: "reasoning mode must be a string".to_string(),
});
};
if mode.trim().is_empty() {
return Err(OpenAiReasoningContractViolation {
kind: OpenAiReasoningViolationKind::InvalidEnum,
field: "reasoning.mode".to_string(),
value: Some(mode.to_string()),
reason: "reasoning mode must not be empty".to_string(),
});
}
if !matches!(mode, "standard" | "pro") {
return Err(OpenAiReasoningContractViolation {
kind: OpenAiReasoningViolationKind::InvalidEnum,
field: "reasoning.mode".to_string(),
value: Some(mode.to_string()),
reason: "reasoning mode supports standard or pro".to_string(),
});
}
let supported = supports_reasoning_mode.unwrap_or_else(|| {
crate::formats::shared::model_directives::openai_model_resolves_to_gpt_5_6(
provider_model,
source_model,
)
});
if supported {
return Ok(());
}
Err(OpenAiReasoningContractViolation {
kind: OpenAiReasoningViolationKind::UnsupportedForModel,
field: "reasoning.mode".to_string(),
value: Some(mode.to_string()),
reason: "provider model does not support reasoning mode".to_string(),
})
}
fn validate_reasoning_context(value: &Value) -> Result<(), OpenAiReasoningContractViolation> {
let Some(context) = value.as_str() else {
return Err(OpenAiReasoningContractViolation {
kind: OpenAiReasoningViolationKind::InvalidType,
field: "reasoning.context".to_string(),
value: Some(value.to_string()),
reason: "reasoning context must be a string".to_string(),
});
};
if matches!(context, "auto" | "current_turn" | "all_turns") {
return Ok(());
}
Err(OpenAiReasoningContractViolation {
kind: OpenAiReasoningViolationKind::InvalidEnum,
field: "reasoning.context".to_string(),
value: Some(context.to_string()),
reason: "reasoning context is not a supported wire value".to_string(),
})
}
fn validate_reasoning_summary(value: &Value) -> Result<(), OpenAiReasoningContractViolation> {
let Some(summary) = value.as_str() else {
return Err(OpenAiReasoningContractViolation {
kind: OpenAiReasoningViolationKind::InvalidType,
field: "reasoning.summary".to_string(),
value: Some(value.to_string()),
reason: "reasoning summary must be a string".to_string(),
});
};
if matches!(summary, "auto" | "concise" | "detailed") {
return Ok(());
}
Err(OpenAiReasoningContractViolation {
kind: OpenAiReasoningViolationKind::InvalidEnum,
field: "reasoning.summary".to_string(),
value: Some(summary.to_string()),
reason: "reasoning summary is not a supported wire value".to_string(),
})
}
#[cfg(test)]
mod tests {
use serde_json::json;
use super::{validate_openai_reasoning_request, OpenAiReasoningViolationKind};
#[test]
fn mapped_model_is_authoritative_for_openai_reasoning_effort() {
let alias = json!({
"model": "deployment-alias",
"reasoning": {"effort": "max"}
});
validate_openai_reasoning_request(
"openai:responses",
"openai:responses",
"gpt-5.6-sol",
&alias,
)
.expect("GPT-5.6 should accept max");
let error = validate_openai_reasoning_request(
"openai:responses",
"openai:responses",
"gpt-5.4",
&alias,
)
.expect_err("GPT-5.4 should reject max");
assert_eq!(
error.kind,
OpenAiReasoningViolationKind::UnsupportedForModel
);
}
#[test]
fn gpt_5_6_rejects_known_unsupported_effort_and_preserves_custom_effort() {
let unsupported = json!({
"model": "gpt-5.6-terra",
"reasoning_effort": "minimal"
});
let error = validate_openai_reasoning_request(
"openai:chat",
"openai:chat",
"gpt-5.6-terra",
&unsupported,
)
.expect_err("known unsupported effort should be rejected");
assert_eq!(
error.kind,
OpenAiReasoningViolationKind::UnsupportedForModel
);
validate_openai_reasoning_request(
"openai:responses",
"openai:responses",
"gpt-5.6-terra",
&json!({
"model": "gpt-5.6-terra",
"reasoning": {"effort": "future"}
}),
)
.expect("model-advertised custom effort should pass through");
let ultra = validate_openai_reasoning_request(
"openai:responses",
"openai:responses",
"gpt-5.6-terra",
&json!({"reasoning": {"effort": "ultra"}}),
)
.expect_err("Codex local ultra preset should not enter the OpenAI wire contract");
assert_eq!(ultra.kind, OpenAiReasoningViolationKind::InvalidEnum);
let empty = validate_openai_reasoning_request(
"openai:responses",
"openai:responses",
"gpt-5.6-terra",
&json!({"reasoning": {"effort": " "}}),
)
.expect_err("empty reasoning effort should be rejected");
assert_eq!(empty.kind, OpenAiReasoningViolationKind::InvalidEnum);
}
#[test]
fn reasoning_mode_is_responses_only_and_requires_gpt_5_6() {
for mode in ["standard", "pro"] {
validate_openai_reasoning_request(
"openai:responses",
"openai:responses",
"gpt-5.6-sol",
&json!({"model": "deployment-alias", "reasoning": {"mode": mode}}),
)
.expect("GPT-5.6 should accept reasoning mode");
}
let unsupported = validate_openai_reasoning_request(
"openai:responses",
"openai:responses",
"gpt-5.4",
&json!({"reasoning": {"mode": "pro"}}),
)
.expect_err("earlier GPT models should reject reasoning mode");
assert_eq!(
unsupported.kind,
OpenAiReasoningViolationKind::UnsupportedForModel
);
let invalid = validate_openai_reasoning_request(
"openai:responses",
"openai:responses",
"gpt-5.6-sol",
&json!({"reasoning": {"mode": "fast"}}),
)
.expect_err("unknown reasoning mode should be rejected");
assert_eq!(invalid.kind, OpenAiReasoningViolationKind::InvalidEnum);
let empty = validate_openai_reasoning_request(
"openai:responses",
"openai:responses",
"gpt-5.6-sol",
&json!({"reasoning": {"mode": ""}}),
)
.expect_err("empty reasoning mode should be rejected");
assert_eq!(empty.kind, OpenAiReasoningViolationKind::InvalidEnum);
validate_openai_reasoning_request(
"openai:chat",
"openai:chat",
"gpt-5.4",
&json!({"reasoning": {"mode": "pro"}}),
)
.expect("Chat Completions does not define reasoning.mode");
}
#[test]
fn reasoning_context_validates_wire_values_without_model_gating() {
for context in ["auto", "current_turn", "all_turns"] {
validate_openai_reasoning_request(
"openai:responses",
"openai:responses",
"gpt-5.4",
&json!({"reasoning": {"context": context}}),
)
.expect("reasoning context should remain available to Codex Responses models");
}
let invalid = validate_openai_reasoning_request(
"openai:responses",
"openai:responses",
"gpt-5.6-sol",
&json!({"reasoning": {"context": "session"}}),
)
.expect_err("unknown reasoning context should be rejected");
assert_eq!(invalid.kind, OpenAiReasoningViolationKind::InvalidEnum);
}
#[test]
fn reasoning_summary_accepts_only_openai_wire_values() {
for summary in ["auto", "concise", "detailed"] {
validate_openai_reasoning_request(
"openai:responses",
"openai:responses",
"gpt-5.6-sol",
&json!({"reasoning": {"summary": summary}}),
)
.expect("documented reasoning summary should be accepted");
}
for (summary, expected_kind) in [
(json!("none"), OpenAiReasoningViolationKind::InvalidEnum),
(json!(true), OpenAiReasoningViolationKind::InvalidType),
] {
let error = validate_openai_reasoning_request(
"openai:responses",
"openai:responses",
"gpt-5.6-sol",
&json!({"reasoning": {"summary": summary}}),
)
.expect_err("invalid reasoning summary should be rejected");
assert_eq!(error.kind, expected_kind);
}
}
#[test]
fn nullable_reasoning_fields_are_treated_as_unconfigured() {
for body in [
json!({"model": "gpt-5.6-sol", "reasoning": null}),
json!({
"model": "gpt-5.6-sol",
"reasoning": {"effort": null, "mode": null, "context": null}
}),
] {
validate_openai_reasoning_request(
"openai:responses",
"openai:responses",
"gpt-5.6-sol",
&body,
)
.expect("nullable Responses reasoning fields should be omitted semantically");
}
validate_openai_reasoning_request(
"openai:chat",
"openai:chat",
"gpt-5.6-sol",
&json!({"model": "gpt-5.6-sol", "reasoning_effort": null}),
)
.expect("nullable Chat reasoning effort should be omitted semantically");
}
}
@@ -0,0 +1,833 @@
use serde_json::Value;
use super::prompt_cache::OpenAiPromptCacheContractViolation;
use super::reasoning::OpenAiReasoningContractViolation;
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum OpenAiProviderRequestContractViolation {
CodexCompact(super::responses::codex::CodexOpenAiCompactRequestContractViolation),
Responses(super::responses::request::OpenAiResponsesRequestContractViolation),
PromptCache(OpenAiPromptCacheContractViolation),
Reasoning(OpenAiReasoningContractViolation),
}
#[derive(Clone, Copy, Debug)]
pub struct OpenAiProviderRequestFinalization<'a> {
pub source_api_format: &'a str,
pub provider_api_format: &'a str,
pub provider_type: &'a str,
pub provider_model: &'a str,
pub source_model: &'a str,
pub body_rules: Option<&'a Value>,
pub upstream_is_stream: bool,
pub require_body_stream_field: bool,
}
pub fn finalize_openai_provider_request(
body: &mut Value,
finalization: OpenAiProviderRequestFinalization<'_>,
) -> Result<(), OpenAiProviderRequestContractViolation> {
finalize_openai_provider_request_with_codex_model_capabilities(body, finalization, None)
}
pub fn finalize_openai_provider_request_with_codex_model_capabilities(
body: &mut Value,
finalization: OpenAiProviderRequestFinalization<'_>,
model_capabilities: Option<&super::responses::codex::CodexResponsesModelCapabilities>,
) -> Result<(), OpenAiProviderRequestContractViolation> {
let is_codex_reasoning_endpoint = finalization
.provider_type
.trim()
.eq_ignore_ascii_case("codex")
&& (crate::is_openai_responses_family_format(finalization.provider_api_format)
|| crate::api_format_alias_matches(finalization.provider_api_format, "openai:search"));
let resolved_model_capabilities = (is_codex_reasoning_endpoint && model_capabilities.is_none())
.then(|| {
super::responses::codex::resolve_codex_responses_model_capabilities(
finalization.provider_model,
finalization.source_model,
None,
)
});
let model_capabilities = is_codex_reasoning_endpoint
.then(|| model_capabilities.or(resolved_model_capabilities.as_ref()))
.flatten();
match crate::normalize_api_format_alias(finalization.source_api_format).as_str() {
"openai:responses" | "openai:responses:compact" => {
super::responses::codex::apply_codex_openai_responses_special_body_edits_with_source_model_and_capabilities(
body,
finalization.provider_type,
finalization.provider_api_format,
finalization.provider_model,
finalization.source_model,
model_capabilities,
finalization.body_rules,
);
}
_ => {
super::responses::codex::apply_codex_openai_responses_chat_body_edits_with_source_model_and_capabilities(
body,
finalization.provider_type,
finalization.provider_api_format,
finalization.provider_model,
finalization.source_model,
model_capabilities,
finalization.body_rules,
)
}
}
super::responses::codex::normalize_codex_openai_reasoning_wire_effort(
body,
finalization.provider_type,
finalization.provider_api_format,
);
super::responses::codex::apply_openai_responses_compact_special_body_edits(
body,
finalization.provider_api_format,
);
crate::enforce_request_body_stream_field(
body,
finalization.provider_api_format,
finalization.upstream_is_stream,
finalization.require_body_stream_field,
);
super::search::apply_openai_search_request_projection(body, finalization.provider_api_format);
let provider_model = body
.get("model")
.and_then(Value::as_str)
.map(str::trim)
.filter(|value| !value.is_empty())
.unwrap_or(finalization.provider_model);
super::responses::codex::validate_codex_openai_responses_compact_request_contract(
body,
finalization.provider_type,
finalization.provider_api_format,
)
.map_err(OpenAiProviderRequestContractViolation::CodexCompact)?;
validate_openai_provider_request_contract_with_codex_model_capabilities(
finalization.provider_api_format,
provider_model,
finalization.source_model,
body,
model_capabilities,
)
}
pub fn validate_openai_provider_request_contract(
provider_api_format: &str,
provider_model: &str,
source_model: &str,
body: &Value,
) -> Result<(), OpenAiProviderRequestContractViolation> {
validate_openai_provider_request_contract_with_codex_model_capabilities(
provider_api_format,
provider_model,
source_model,
body,
None,
)
}
fn validate_openai_provider_request_contract_with_codex_model_capabilities(
provider_api_format: &str,
provider_model: &str,
source_model: &str,
body: &Value,
model_capabilities: Option<&super::responses::codex::CodexResponsesModelCapabilities>,
) -> Result<(), OpenAiProviderRequestContractViolation> {
super::responses::request::validate_openai_responses_request_contract(
body,
provider_api_format,
)
.map_err(OpenAiProviderRequestContractViolation::Responses)?;
super::prompt_cache::validate_openai_prompt_cache_request_with_source_model(
provider_api_format,
provider_model,
source_model,
body,
)
.map_err(OpenAiProviderRequestContractViolation::PromptCache)?;
super::reasoning::validate_openai_reasoning_request_with_model_profile(
provider_api_format,
provider_api_format,
provider_model,
source_model,
body,
model_capabilities.map(|capabilities| capabilities.supported_reasoning_efforts.as_slice()),
None,
)
.map_err(OpenAiProviderRequestContractViolation::Reasoning)
}
#[cfg(test)]
mod tests {
use serde_json::json;
use super::{
finalize_openai_provider_request,
finalize_openai_provider_request_with_codex_model_capabilities,
validate_openai_provider_request_contract, OpenAiProviderRequestFinalization,
};
use crate::CodexResponsesModelCapabilities;
#[test]
fn validates_reasoning_and_prompt_cache_against_the_final_provider_model() {
let body = json!({
"model": "gpt-5.6-sol",
"input": [],
"reasoning": {"effort": "max"},
"prompt_cache_options": {"mode": "explicit", "ttl": "30m"}
});
validate_openai_provider_request_contract(
"openai:responses",
"gpt-5.6-sol",
"gpt-5.6-sol",
&body,
)
.expect("GPT-5.6 request should satisfy the final provider contract");
assert!(validate_openai_provider_request_contract(
"openai:responses",
"gpt-5.4",
"gpt-5.6-sol",
&body,
)
.is_err());
}
#[test]
fn opaque_provider_models_inherit_source_capabilities_but_concrete_models_do_not() {
let body = json!({
"model": "azure-production",
"input": [],
"reasoning": {"effort": "max", "mode": "pro"},
"prompt_cache_options": {"mode": "explicit", "ttl": "30m"}
});
validate_openai_provider_request_contract(
"openai:responses",
"azure-production",
"gpt-5.6-sol-max",
&body,
)
.expect("opaque deployments should inherit the concrete source model capability");
assert!(validate_openai_provider_request_contract(
"openai:responses",
"gpt-5.4",
"gpt-5.6-sol",
&body,
)
.is_err());
}
#[test]
fn codex_finalization_enforces_model_card_reasoning_efforts() {
let mut body = json!({
"model": "gpt-5.6-sol",
"input": [],
"reasoning": {"effort": "minimal"}
});
let error = finalize_openai_provider_request(
&mut body,
OpenAiProviderRequestFinalization {
source_api_format: "openai:responses",
provider_api_format: "openai:responses",
provider_type: "codex",
provider_model: "gpt-5.6-sol",
source_model: "gpt-5.6-sol",
body_rules: None,
upstream_is_stream: false,
require_body_stream_field: true,
},
)
.expect_err("GPT-5.6 Codex model card should reject minimal");
assert!(matches!(
error,
super::OpenAiProviderRequestContractViolation::Reasoning(
super::OpenAiReasoningContractViolation {
kind: crate::formats::openai::reasoning::OpenAiReasoningViolationKind::UnsupportedForModel,
..
}
)
));
}
#[test]
fn finalization_reapplies_codex_and_compact_projection_after_mutations() {
let mut body = json!({
"model": "gpt-5.6-sol",
"input": [],
"store": true,
"include": ["reasoning.encrypted_content"],
"client_metadata": {"source": "mapping"},
"stream": true,
"stream_options": {"include_usage": true},
"tool_choice": "auto",
"temperature": 0.5,
"previous_response_id": "resp_123"
});
finalize_openai_provider_request(
&mut body,
OpenAiProviderRequestFinalization {
source_api_format: "openai:responses",
provider_api_format: "openai:responses:compact",
provider_type: "codex",
provider_model: "gpt-5.6-sol",
source_model: "gpt-5.6-sol",
body_rules: None,
upstream_is_stream: false,
require_body_stream_field: true,
},
)
.expect("final Compact request should satisfy its provider contract");
for field in [
"store",
"include",
"client_metadata",
"stream",
"stream_options",
"tool_choice",
"temperature",
"previous_response_id",
] {
assert!(body.get(field).is_none(), "{field} must not reach Compact");
}
}
#[test]
fn non_responses_sources_receive_codex_responses_reasoning_defaults() {
for source_api_format in ["openai:chat", "claude:messages", "gemini:generate_content"] {
let mut body = json!({
"model": "gpt-5.6-sol",
"input": []
});
finalize_openai_provider_request(
&mut body,
OpenAiProviderRequestFinalization {
source_api_format,
provider_api_format: "openai:responses",
provider_type: "codex",
provider_model: "gpt-5.6-sol",
source_model: "gpt-5.6-sol",
body_rules: None,
upstream_is_stream: true,
require_body_stream_field: true,
},
)
.expect("Codex Responses request should satisfy the final provider contract");
assert_eq!(body["reasoning"]["effort"], "low");
assert!(body["reasoning"].get("summary").is_none());
}
}
#[test]
fn codex_ultra_preset_uses_max_for_every_codex_model_on_the_wire() {
let mut sol = json!({
"model": "gpt-5.6-sol",
"input": [],
"reasoning": {"effort": "ultra"}
});
let mut luna = json!({
"model": "gpt-5.6-luna",
"input": [],
"reasoning": {"effort": "ultra"}
});
let finalization_for = |model| OpenAiProviderRequestFinalization {
source_api_format: "openai:responses",
provider_api_format: "openai:responses",
provider_type: "codex",
provider_model: model,
source_model: model,
body_rules: None,
upstream_is_stream: true,
require_body_stream_field: true,
};
finalize_openai_provider_request(&mut sol, finalization_for("gpt-5.6-sol"))
.expect("Sol ultra preset should map to the OpenAI wire contract");
assert_eq!(sol["reasoning"]["effort"], "max");
finalize_openai_provider_request(&mut luna, finalization_for("gpt-5.6-luna"))
.expect("Luna ultra preset should map to the OpenAI wire contract");
assert_eq!(luna["reasoning"]["effort"], "max");
}
#[test]
fn dynamic_codex_card_controls_default_effort_and_keeps_mode_model_specific() {
let finalization = OpenAiProviderRequestFinalization {
source_api_format: "openai:responses",
provider_api_format: "openai:responses",
provider_type: "codex",
provider_model: "gpt-5.7-sol",
source_model: "gpt-5.7-sol",
body_rules: None,
upstream_is_stream: true,
require_body_stream_field: true,
};
let capabilities = CodexResponsesModelCapabilities {
use_responses_lite: true,
supports_reasoning_summary_parameter: true,
default_reasoning_effort: Some("ultra".to_string()),
default_reasoning_summary: None,
supported_reasoning_efforts: vec!["max".to_string(), "ultra".to_string()],
supports_parallel_tool_calls: true,
support_verbosity: true,
default_verbosity: Some("low".to_string()),
supported_service_tiers: vec!["priority".to_string()],
};
let mut default_body = json!({"model": "gpt-5.7-sol", "input": []});
finalize_openai_provider_request_with_codex_model_capabilities(
&mut default_body,
finalization,
Some(&capabilities),
)
.expect("card default ultra should use the max wire effort");
assert_eq!(default_body["reasoning"]["effort"], "max");
let mut mode_body = json!({
"model": "gpt-5.7-sol",
"input": [],
"reasoning": {"effort": "max", "mode": "pro"}
});
let mode_error = finalize_openai_provider_request_with_codex_model_capabilities(
&mut mode_body,
finalization,
Some(&capabilities),
)
.expect_err("Responses Lite alone must not enable GPT-5.6 reasoning modes");
assert!(matches!(
mode_error,
super::OpenAiProviderRequestContractViolation::Reasoning(_)
));
let ultra_only = CodexResponsesModelCapabilities {
supported_reasoning_efforts: vec!["ultra".to_string()],
..capabilities.clone()
};
let mut ultra_only_body = json!({
"model": "gpt-5.7-sol",
"input": [],
"reasoning": {"effort": "ultra"}
});
finalize_openai_provider_request_with_codex_model_capabilities(
&mut ultra_only_body,
finalization,
Some(&ultra_only),
)
.expect("Codex maps the Ultra preset to max without card-list wire validation");
assert_eq!(ultra_only_body["reasoning"]["effort"], "max");
}
#[test]
fn codex_search_normalizes_reasoning_and_projects_the_typed_request() {
let finalization = OpenAiProviderRequestFinalization {
source_api_format: "openai:search",
provider_api_format: "openai:search",
provider_type: "codex",
provider_model: "gpt-5.6-sol",
source_model: "gpt-5.6-sol",
body_rules: None,
upstream_is_stream: false,
require_body_stream_field: false,
};
let mut body = json!({
"id": "session-1",
"model": "gpt-5.6-sol",
"reasoning": {
"effort": "ultra",
"summary": "auto",
"context": "current_turn",
"future_reasoning_field": true
},
"commands": {"search_query": [{"q": "Aether"}]},
"store": false,
"future_request_field": {"enabled": true},
"stream": true
});
finalize_openai_provider_request(&mut body, finalization)
.expect("Codex Search request should finalize");
assert_eq!(body["reasoning"]["effort"], "max");
assert_eq!(body["reasoning"]["summary"], "auto");
assert_eq!(body["reasoning"]["context"], "current_turn");
assert!(body["reasoning"].get("future_reasoning_field").is_none());
assert_eq!(body["commands"]["search_query"][0]["q"], "Aether");
assert!(body.get("store").is_none());
assert!(body.get("future_request_field").is_none());
assert!(body.get("stream").is_none());
assert!(body.get("tool_choice").is_none());
assert!(body.get("include").is_none());
}
#[test]
fn codex_search_validates_reasoning_effort_against_model_card() {
let finalization = OpenAiProviderRequestFinalization {
source_api_format: "openai:search",
provider_api_format: "openai:search",
provider_type: "codex",
provider_model: "gpt-5.6-sol",
source_model: "gpt-5.6-sol",
body_rules: None,
upstream_is_stream: false,
require_body_stream_field: false,
};
let mut supported = json!({
"id": "session-1",
"model": "gpt-5.6-sol",
"reasoning": {"effort": "high"}
});
finalize_openai_provider_request(&mut supported, finalization)
.expect("published Search effort should pass");
let mut unsupported = json!({
"id": "session-1",
"model": "gpt-5.6-sol",
"reasoning": {"effort": "none"}
});
let error = finalize_openai_provider_request(&mut unsupported, finalization)
.expect_err("unpublished Search effort should be rejected");
assert!(matches!(
error,
super::OpenAiProviderRequestContractViolation::Reasoning(_)
));
}
#[test]
fn dynamic_codex_card_preserves_custom_reasoning_effort_case() {
let finalization = OpenAiProviderRequestFinalization {
source_api_format: "openai:responses",
provider_api_format: "openai:responses",
provider_type: "codex",
provider_model: "codex-custom",
source_model: "codex-custom",
body_rules: None,
upstream_is_stream: true,
require_body_stream_field: true,
};
let capabilities = CodexResponsesModelCapabilities {
use_responses_lite: false,
supports_reasoning_summary_parameter: true,
default_reasoning_effort: Some("VendorEffortX".to_string()),
default_reasoning_summary: None,
supported_reasoning_efforts: vec!["VendorEffortX".to_string()],
supports_parallel_tool_calls: true,
support_verbosity: true,
default_verbosity: Some("low".to_string()),
supported_service_tiers: vec![],
};
let mut body = json!({"model": "codex-custom", "input": []});
finalize_openai_provider_request_with_codex_model_capabilities(
&mut body,
finalization,
Some(&capabilities),
)
.expect("custom card effort should remain exact");
assert_eq!(body["reasoning"]["effort"], "VendorEffortX");
let mut custom = json!({
"model": "codex-custom",
"input": [],
"reasoning": {"effort": "vendoreffortx"}
});
let error = finalize_openai_provider_request_with_codex_model_capabilities(
&mut custom,
finalization,
Some(&capabilities),
)
.expect_err("custom reasoning efforts should match the model card exactly");
assert!(matches!(
error,
super::OpenAiProviderRequestContractViolation::Reasoning(_)
));
let mut ultra = json!({
"model": "codex-custom",
"input": [],
"reasoning": {"effort": "ultra"}
});
let error = finalize_openai_provider_request_with_codex_model_capabilities(
&mut ultra,
finalization,
Some(&capabilities),
)
.expect_err("ultra should require model-card support before mapping to max");
assert!(matches!(
error,
super::OpenAiProviderRequestContractViolation::Reasoning(_)
));
}
#[test]
fn gpt_5_6_sol_uses_the_responses_lite_request_contract() {
let mut body = json!({
"model": "gpt-5.6-sol",
"instructions": "Follow the project instructions.",
"input": [
{
"type": "message",
"role": "user",
"content": [{
"type": "input_image",
"image_url": "data:image/png;base64,aGVsbG8=",
"detail": "original"
}]
},
{
"type": "function_call_output",
"call_id": "call-1",
"output": [{
"type": "input_image",
"image_url": "data:image/png;base64,ZnVuY3Rpb24=",
"detail": "high"
}]
},
{
"type": "custom_tool_call_output",
"call_id": "call-2",
"output": [{
"type": "input_image",
"image_url": "data:image/png;base64,Y3VzdG9t",
"detail": "auto"
}]
}
],
"tools": [{
"type": "function",
"name": "lookup",
"parameters": {
"type": "object",
"properties": {
"detail": {"type": "string"}
}
}
}],
"parallel_tool_calls": true
});
let finalization = OpenAiProviderRequestFinalization {
source_api_format: "openai:responses",
provider_api_format: "openai:responses",
provider_type: "codex",
provider_model: "gpt-5.6-sol",
source_model: "gpt-5.6-sol",
body_rules: None,
upstream_is_stream: true,
require_body_stream_field: true,
};
finalize_openai_provider_request(&mut body, finalization)
.expect("GPT-5.6 Sol should satisfy the Responses Lite contract");
let first = body.clone();
finalize_openai_provider_request(&mut body, finalization)
.expect("Responses Lite finalization should be idempotent");
assert_eq!(body, first);
assert!(body.get("instructions").is_none());
assert!(body.get("tools").is_none());
assert_eq!(body["input"][0]["type"], "additional_tools");
assert_eq!(body["input"][0]["role"], "developer");
assert_eq!(body["input"][0]["tools"][0]["name"], "lookup");
assert_eq!(body["input"][1]["type"], "message");
assert_eq!(body["input"][1]["role"], "developer");
assert_eq!(
body["input"][1]["content"][0]["text"],
"Follow the project instructions."
);
assert!(body["input"][2]["content"][0].get("detail").is_none());
assert!(body["input"][3]["output"][0].get("detail").is_none());
assert!(body["input"][4]["output"][0].get("detail").is_none());
assert_eq!(
body["input"][0]["tools"][0]["parameters"]["properties"]["detail"]["type"],
"string"
);
assert_eq!(body["parallel_tool_calls"], false);
assert_eq!(body["reasoning"]["effort"], "low");
assert_eq!(body["reasoning"]["context"], "all_turns");
assert!(body["reasoning"].get("summary").is_none());
}
#[test]
fn gpt_5_6_sol_uses_standard_responses_contract_for_server_side_compaction() {
let mut body = json!({
"model": "gpt-5.6-sol",
"instructions": "Preserve the standard Responses request shape.",
"input": [{
"type": "message",
"role": "user",
"content": [{"type": "input_text", "text": "compact"}]
}],
"tools": [{"type": "function", "name": "lookup"}],
"context_management": [{
"type": "compaction",
"compact_threshold": 128000
}],
"parallel_tool_calls": true
});
let finalization = OpenAiProviderRequestFinalization {
source_api_format: "openai:responses",
provider_api_format: "openai:responses",
provider_type: "codex",
provider_model: "gpt-5.6-sol",
source_model: "gpt-5.6-sol",
body_rules: None,
upstream_is_stream: true,
require_body_stream_field: true,
};
finalize_openai_provider_request(&mut body, finalization)
.expect("server-side compaction should use the standard Responses contract");
let first = body.clone();
finalize_openai_provider_request(&mut body, finalization)
.expect("standard Responses finalization should be idempotent");
assert_eq!(body, first);
assert_eq!(body["context_management"][0]["compact_threshold"], 128000);
assert_eq!(
body["instructions"],
"Preserve the standard Responses request shape."
);
assert_eq!(body["tools"][0]["name"], "lookup");
assert_eq!(body["parallel_tool_calls"], true);
assert!(body["reasoning"].get("context").is_none());
}
#[test]
fn opaque_codex_deployments_use_the_exact_source_model_card() {
let mut body = json!({
"model": "azure-production",
"instructions": "Use the configured tools.",
"input": [],
"tools": [],
"parallel_tool_calls": true
});
finalize_openai_provider_request(
&mut body,
OpenAiProviderRequestFinalization {
source_api_format: "openai:responses",
provider_api_format: "openai:responses:compact",
provider_type: "codex",
provider_model: "azure-production",
source_model: "gpt-5.6-terra",
body_rules: None,
upstream_is_stream: false,
require_body_stream_field: true,
},
)
.expect("opaque Codex deployment should use the exact source model card");
assert!(body.get("instructions").is_none());
assert!(body.get("tools").is_none());
assert_eq!(body["input"][0]["type"], "additional_tools");
assert_eq!(body["input"][1]["role"], "developer");
assert_eq!(body["parallel_tool_calls"], false);
assert_eq!(body["reasoning"]["effort"], "medium");
assert_eq!(body["reasoning"]["context"], "all_turns");
for field in body.as_object().expect("object").keys() {
assert!(
[
"model",
"input",
"instructions",
"tools",
"parallel_tool_calls",
"reasoning",
"service_tier",
"prompt_cache_key",
"text",
]
.contains(&field.as_str()),
"unexpected Compact field: {field}"
);
}
}
#[test]
fn gpt_5_4_keeps_the_standard_codex_responses_shape() {
let mut body = json!({
"model": "gpt-5.4",
"instructions": "Keep this top-level instruction.",
"input": [],
"tools": [{"type": "function", "name": "lookup", "parameters": {}}],
"parallel_tool_calls": true
});
finalize_openai_provider_request(
&mut body,
OpenAiProviderRequestFinalization {
source_api_format: "openai:responses",
provider_api_format: "openai:responses",
provider_type: "codex",
provider_model: "gpt-5.4",
source_model: "gpt-5.4",
body_rules: None,
upstream_is_stream: true,
require_body_stream_field: true,
},
)
.expect("GPT-5.4 should satisfy the standard Codex Responses contract");
assert_eq!(body["instructions"], "Keep this top-level instruction.");
assert_eq!(body["tools"][0]["name"], "lookup");
assert_eq!(body["parallel_tool_calls"], true);
assert_eq!(body["reasoning"]["effort"], "medium");
assert!(body["reasoning"].get("context").is_none());
assert!(body["reasoning"].get("summary").is_none());
}
#[test]
fn cross_format_compact_finalization_removes_post_conversion_fields() {
for source_api_format in ["claude:messages", "gemini:generate_content"] {
let mut body = json!({
"model": "gpt-5.6-sol",
"input": [],
"client_metadata": {"source": "mapping"},
"include": ["reasoning.encrypted_content"],
"store": true,
"stream": true,
"stream_options": {"include_usage": true},
"tool_choice": "auto",
"parallel_tool_calls": true,
"reasoning": {"effort": "max"},
"text": {"verbosity": "medium"},
"tools": [{"type": "function", "name": "lookup", "parameters": {}}]
});
finalize_openai_provider_request(
&mut body,
OpenAiProviderRequestFinalization {
source_api_format,
provider_api_format: "openai:responses:compact",
provider_type: "codex",
provider_model: "gpt-5.6-sol",
source_model: "gpt-5.6-sol",
body_rules: None,
upstream_is_stream: false,
require_body_stream_field: true,
},
)
.expect("cross-format Compact request should satisfy its final contract");
for field in [
"client_metadata",
"include",
"store",
"stream",
"stream_options",
"tool_choice",
] {
assert!(body.get(field).is_none(), "{field} must not reach Compact");
}
assert_eq!(body["parallel_tool_calls"], false);
assert_eq!(body["reasoning"]["effort"], "max");
assert_eq!(body["reasoning"]["context"], "all_turns");
assert_eq!(body["text"]["verbosity"], "medium");
assert!(body.get("tools").is_none());
assert_eq!(body["input"][0]["tools"][0]["name"], "lookup");
}
}
}
@@ -0,0 +1 @@
pub mod request;
@@ -0,0 +1,113 @@
use serde_json::Map;
use serde_json::Value;
use crate::formats::context::FormatContext;
use crate::formats::openai::embedding::request::namespace_extensions;
use crate::protocol::canonical::{
namespace_extension_object, CanonicalRequest, CanonicalRerankRequest,
};
pub fn from(body: &Value, _ctx: &FormatContext) -> Option<CanonicalRequest> {
from_namespace(body, "openai")
}
pub fn to(request: &CanonicalRequest, ctx: &FormatContext) -> Option<Value> {
to_openai_like(
request,
ctx.mapped_model_or(request.model.as_str()),
"openai",
)
}
pub(crate) fn from_namespace(body_json: &Value, namespace: &str) -> Option<CanonicalRequest> {
let request = body_json.as_object()?;
let model = request
.get("model")
.and_then(Value::as_str)
.map(str::trim)
.filter(|value| !value.is_empty())?
.to_string();
let query = request
.get("query")
.and_then(Value::as_str)
.map(str::trim)
.filter(|value| !value.is_empty())?
.to_string();
let documents = request.get("documents").and_then(Value::as_array)?.to_vec();
let rerank = CanonicalRerankRequest {
query,
documents,
top_n: request
.get("top_n")
.or_else(|| request.get("topN"))
.and_then(Value::as_u64),
return_documents: request
.get("return_documents")
.or_else(|| request.get("returnDocuments"))
.and_then(Value::as_bool),
extensions: namespace_extensions(
namespace,
request,
&[
"model",
"query",
"documents",
"top_n",
"topN",
"return_documents",
"returnDocuments",
],
),
};
if rerank.is_empty() || rerank.top_n == Some(0) {
return None;
}
Some(CanonicalRequest {
model,
rerank: Some(rerank),
..CanonicalRequest::default()
})
}
pub(crate) fn to_openai_like(
canonical: &CanonicalRequest,
mapped_model: &str,
namespace: &str,
) -> Option<Value> {
let rerank = canonical.rerank.as_ref()?;
if rerank.is_empty() || rerank.top_n == Some(0) {
return None;
}
let mut output = Map::new();
output.insert(
"model".to_string(),
Value::String(mapped_rerank_model(canonical, mapped_model)),
);
output.insert("query".to_string(), Value::String(rerank.query.clone()));
output.insert(
"documents".to_string(),
Value::Array(rerank.documents.clone()),
);
if let Some(value) = rerank.top_n {
output.insert("top_n".to_string(), Value::from(value));
}
if let Some(value) = rerank.return_documents {
output.insert("return_documents".to_string(), Value::Bool(value));
}
output.extend(namespace_extension_object(
&rerank.extensions,
namespace,
&output,
));
Some(Value::Object(output))
}
fn mapped_rerank_model(canonical: &CanonicalRequest, mapped_model: &str) -> String {
let mapped_model = mapped_model.trim();
if mapped_model.is_empty() {
canonical.model.clone()
} else {
mapped_model.to_string()
}
}
File diff suppressed because it is too large Load Diff
@@ -0,0 +1,91 @@
use serde_json::Value;
pub mod codex;
pub mod request;
pub mod response;
pub mod spec;
pub mod stream;
const TOOL_ERROR_PREFIX: &str = "[tool error]";
/// Semantic operation carried by an OpenAI Responses request that asks the
/// service to compact a thread. The request still uses the Responses wire
/// contract and transport endpoint.
pub const OPENAI_RESPONSES_OPERATION_COMPACT: &str = "compact";
/// Resolves the operation expressed by an OpenAI Responses wire request.
///
/// `responses_compaction_v2` is represented by a `compaction_trigger` input
/// item on the normal Responses request. The legacy Compact API format is
/// retained as the same operation for observability and scoped model mapping.
pub fn openai_responses_request_operation(api_format: &str, body: &Value) -> Option<&'static str> {
if aether_ai_formats::is_openai_responses_compact_format(api_format) {
return Some(OPENAI_RESPONSES_OPERATION_COMPACT);
}
if !aether_ai_formats::is_openai_responses_format(api_format) {
return None;
}
body.get("input")
.and_then(Value::as_array)
.is_some_and(|items| {
items
.iter()
.any(|item| item.get("type").and_then(Value::as_str) == Some("compaction_trigger"))
})
.then_some(OPENAI_RESPONSES_OPERATION_COMPACT)
}
fn encode_tool_result_error(output: Value, is_error: bool) -> Value {
if !is_error {
return output;
}
let detail = match output {
Value::String(text) => text,
Value::Null => String::new(),
value => serde_json::to_string(&value).unwrap_or_else(|_| value.to_string()),
};
if detail.is_empty() {
Value::String(TOOL_ERROR_PREFIX.to_string())
} else {
Value::String(format!("{TOOL_ERROR_PREFIX}\n{detail}"))
}
}
#[cfg(test)]
mod tests {
use serde_json::json;
use super::{openai_responses_request_operation, OPENAI_RESPONSES_OPERATION_COMPACT};
#[test]
fn resolves_compaction_trigger_as_compact_operation_on_responses_transport() {
assert_eq!(
openai_responses_request_operation(
"openai:responses",
&json!({
"input": [
{"role": "user", "content": "keep working"},
{"type": "compaction_trigger"}
]
}),
),
Some(OPENAI_RESPONSES_OPERATION_COMPACT)
);
assert_eq!(
openai_responses_request_operation(
"openai:responses",
&json!({"input": [{"role": "user", "content": "keep working"}]}),
),
None
);
}
#[test]
fn resolves_legacy_compact_contract_without_a_body_marker() {
assert_eq!(
openai_responses_request_operation("openai:responses:compact", &json!({})),
Some(OPENAI_RESPONSES_OPERATION_COMPACT)
);
}
}
File diff suppressed because it is too large Load Diff
@@ -0,0 +1,771 @@
use std::{
collections::BTreeMap,
time::{SystemTime, UNIX_EPOCH},
};
use serde_json::{json, Map, Value};
use super::encode_tool_result_error;
use crate::{
formats::context::FormatContext,
protocol::canonical::{
canonical_content_block_to_openai_responses_part, canonical_extension_object_mut,
canonical_tool_use_to_openai_responses_item, canonical_usage_to_openai_responses_usage,
flush_openai_responses_message_item, is_openai_responses_raw_block,
is_openai_thinking_block, namespace_extension_object, openai_responses_extensions,
openai_responses_item_extension_object, openai_responses_output_to_canonical,
openai_responses_usage_to_canonical, openai_service_tier_extension, CanonicalContentBlock,
CanonicalResponse, CanonicalResponseOutput, CanonicalRole, CanonicalStopReason,
OPENAI_RESPONSES_EXTENSION_NAMESPACE, OPENAI_RESPONSES_LEGACY_EXTENSION_NAMESPACE,
},
};
const AETHER_RESPONSES_RAW_OUTPUT_KEY: &str = "openai_responses_raw_output";
pub fn from(body: &Value, _ctx: &FormatContext) -> Option<CanonicalResponse> {
from_raw(body)
}
pub fn to(response: &CanonicalResponse, ctx: &FormatContext) -> Option<Value> {
Some(to_raw(response, &ctx.report_context_value(), false))
}
pub fn to_compact(response: &CanonicalResponse, ctx: &FormatContext) -> Option<Value> {
Some(to_raw(response, &ctx.report_context_value(), true))
}
pub fn from_raw(body_json: &Value) -> Option<CanonicalResponse> {
let body = body_json.as_object()?;
if body.get("error").is_some_and(|error| !error.is_null())
|| body.get("status").and_then(Value::as_str) == Some("failed")
{
return None;
}
let (content, output_extensions) = openai_responses_output_to_canonical(body.get("output"))?;
let has_tool_use = content
.iter()
.any(|block| matches!(block, CanonicalContentBlock::ToolUse { .. }));
let stop_reason = if has_tool_use {
Some(CanonicalStopReason::ToolUse)
} else {
match body.get("status").and_then(Value::as_str) {
Some("incomplete") => Some(openai_responses_incomplete_stop_reason(body)),
Some("failed") => Some(CanonicalStopReason::Unknown),
_ => Some(CanonicalStopReason::EndTurn),
}
};
let mut extensions = openai_responses_extensions(
body,
&[
"id", "object", "model", "output", "usage", "status", "error",
],
);
if let Some(raw_status) = body.get("status").cloned() {
canonical_extension_object_mut(&mut extensions, OPENAI_RESPONSES_EXTENSION_NAMESPACE)
.insert("raw_status".to_string(), raw_status);
}
if let Some(raw_output) = body.get("output").cloned() {
canonical_extension_object_mut(&mut extensions, "aether")
.insert(AETHER_RESPONSES_RAW_OUTPUT_KEY.to_string(), raw_output);
}
Some(CanonicalResponse {
id: body
.get("id")
.and_then(Value::as_str)
.unwrap_or("resp-unknown")
.to_string(),
model: body
.get("model")
.and_then(Value::as_str)
.unwrap_or("unknown")
.to_string(),
outputs: vec![CanonicalResponseOutput {
index: 0,
role: CanonicalRole::Assistant,
content: content.clone(),
stop_reason: stop_reason.clone(),
extensions: output_extensions,
}],
content,
stop_reason,
usage: openai_responses_usage_to_canonical(body.get("usage")),
extensions,
})
}
fn openai_responses_incomplete_stop_reason(body: &Map<String, Value>) -> CanonicalStopReason {
match body
.get("incomplete_details")
.and_then(Value::as_object)
.and_then(|details| details.get("reason"))
.and_then(Value::as_str)
{
Some("content_filter") => CanonicalStopReason::ContentFiltered,
Some("tool_calls") | Some("function_call") => CanonicalStopReason::ToolUse,
_ => CanonicalStopReason::MaxTokens,
}
}
pub fn to_raw(canonical: &CanonicalResponse, report_context: &Value, compact: bool) -> Value {
let mut response = Map::new();
let response_id = canonical.id.replace("chatcmpl", "resp");
response.insert("id".to_string(), Value::String(response_id.clone()));
response.insert(
"object".to_string(),
Value::String(if compact {
"response.compaction".to_string()
} else {
"response".to_string()
}),
);
if !compact {
response.insert("status".to_string(), Value::String("completed".to_string()));
response.insert("model".to_string(), Value::String(canonical.model.clone()));
if let Some(raw_status) = canonical
.extensions
.get(OPENAI_RESPONSES_EXTENSION_NAMESPACE)
.or_else(|| {
canonical
.extensions
.get(OPENAI_RESPONSES_LEGACY_EXTENSION_NAMESPACE)
})
.and_then(Value::as_object)
.and_then(|openai| openai.get("raw_status"))
.cloned()
{
response.insert("status".to_string(), raw_status);
}
}
let mut output = Vec::new();
let mut message_content = Vec::new();
let mut message_index = 0usize;
for block in &canonical.content {
match block {
CanonicalContentBlock::Text { .. }
| CanonicalContentBlock::File { .. }
| CanonicalContentBlock::Audio { .. } => {
if let Some(part) = canonical_content_block_to_openai_responses_part(block) {
message_content.push(part);
}
}
CanonicalContentBlock::Image {
data,
url,
media_type,
extensions,
..
} => {
if image_block_is_generation_call(extensions) {
flush_openai_responses_message_item(
&mut output,
&mut message_content,
&response_id,
&mut message_index,
);
output.push(openai_responses_image_generation_call_item(
&response_id,
output.len(),
data,
url,
media_type,
));
} else if let Some(part) = canonical_content_block_to_openai_responses_part(block) {
message_content.push(part);
}
}
CanonicalContentBlock::Thinking {
text,
encrypted_content,
extensions,
..
} => {
let encrypted_content = encrypted_content
.as_ref()
.filter(|value| !value.is_empty())
.filter(|_| is_openai_thinking_block(extensions));
if text.trim().is_empty() && encrypted_content.is_none() {
continue;
}
flush_openai_responses_message_item(
&mut output,
&mut message_content,
&response_id,
&mut message_index,
);
let mut item = Map::new();
item.insert("type".to_string(), Value::String("reasoning".to_string()));
item.insert(
"id".to_string(),
Value::String(format!("{}_rs_{}", response_id, output.len())),
);
item.insert("status".to_string(), Value::String("completed".to_string()));
if let Some(encrypted_content) = encrypted_content {
item.insert(
"encrypted_content".to_string(),
Value::String(encrypted_content.clone()),
);
}
if !text.trim().is_empty() {
item.insert(
"summary".to_string(),
Value::Array(vec![json!({
"type": "summary_text",
"text": text,
})]),
);
}
output.push(Value::Object(item));
}
CanonicalContentBlock::ToolUse {
id,
name,
input,
extensions,
} => {
flush_openai_responses_message_item(
&mut output,
&mut message_content,
&response_id,
&mut message_index,
);
if is_responses_web_search_tool(name) {
output.push(json!({
"type": "web_search_call",
"id": id,
"status": "completed",
"action": {
"type": "search",
"query": web_search_query_from_value(input),
},
}));
} else {
output.push(canonical_tool_use_to_openai_responses_item(
id, name, input, extensions,
));
}
}
CanonicalContentBlock::ToolResult {
tool_use_id,
output: result_output,
content_text,
is_error,
extensions,
..
} => {
flush_openai_responses_message_item(
&mut output,
&mut message_content,
&response_id,
&mut message_index,
);
let mut item = Map::new();
item.insert(
"type".to_string(),
Value::String(
responses_tool_result_item_type(extensions)
.unwrap_or("function_call_output")
.to_string(),
),
);
item.insert("call_id".to_string(), Value::String(tool_use_id.clone()));
item.insert(
"output".to_string(),
encode_tool_result_error(
result_output.clone().unwrap_or_else(|| {
Value::String(content_text.clone().unwrap_or_default())
}),
*is_error,
),
);
let extension_fields = openai_responses_item_extension_object(extensions, &item);
item.extend(extension_fields);
output.push(Value::Object(item));
}
CanonicalContentBlock::Unknown {
raw_type, payload, ..
} if raw_type == "refusal" => {
if let Some(text) = payload.get("refusal").and_then(Value::as_str) {
if !text.trim().is_empty() {
message_content.push(json!({
"type": "refusal",
"refusal": text,
}));
}
}
}
CanonicalContentBlock::Unknown {
payload,
extensions,
..
} if is_openai_responses_raw_block(extensions) => {
flush_openai_responses_message_item(
&mut output,
&mut message_content,
&response_id,
&mut message_index,
);
output.push(payload.clone());
}
CanonicalContentBlock::Unknown { .. } => {}
}
}
flush_openai_responses_message_item(
&mut output,
&mut message_content,
&response_id,
&mut message_index,
);
if let Some(raw_output) = canonical
.extensions
.get("aether")
.and_then(Value::as_object)
.and_then(|aether| aether.get(AETHER_RESPONSES_RAW_OUTPUT_KEY))
.and_then(Value::as_array)
{
output.clone_from(raw_output);
}
response.insert("output".to_string(), Value::Array(output));
if let Some(usage) = &canonical.usage {
response.insert(
"usage".to_string(),
canonical_usage_to_openai_responses_usage(usage),
);
}
if compact {
let created_at = canonical
.extensions
.get(OPENAI_RESPONSES_EXTENSION_NAMESPACE)
.or_else(|| {
canonical
.extensions
.get(OPENAI_RESPONSES_LEGACY_EXTENSION_NAMESPACE)
})
.and_then(Value::as_object)
.and_then(|openai| openai.get("created_at").or_else(|| openai.get("created")))
.and_then(openai_responses_timestamp_value)
.unwrap_or_else(openai_responses_current_timestamp);
response.insert("created_at".to_string(), Value::from(created_at));
return Value::Object(response);
}
if let Some(request_object) = report_context
.get("original_request_body")
.and_then(Value::as_object)
{
for key in [
"instructions",
"max_output_tokens",
"parallel_tool_calls",
"previous_response_id",
"reasoning",
"store",
"temperature",
"text",
"tool_choice",
"tools",
"top_p",
"truncation",
"user",
"metadata",
] {
if let Some(value) = request_object.get(key) {
response.insert(key.to_string(), value.clone());
}
}
}
if let Some(service_tier) = openai_service_tier_extension(&canonical.extensions).cloned() {
response.insert("service_tier".to_string(), service_tier);
}
let mut extension_fields = namespace_extension_object(
&canonical.extensions,
OPENAI_RESPONSES_EXTENSION_NAMESPACE,
&response,
);
extension_fields.remove("raw_status");
response.extend(extension_fields);
let mut legacy_extension_fields = namespace_extension_object(
&canonical.extensions,
OPENAI_RESPONSES_LEGACY_EXTENSION_NAMESPACE,
&response,
);
legacy_extension_fields.remove("raw_status");
response.extend(legacy_extension_fields);
ensure_modern_openai_responses_response_fields(&mut response);
Value::Object(response)
}
fn responses_tool_result_item_type(extensions: &BTreeMap<String, Value>) -> Option<&str> {
let item_type = extensions
.get(OPENAI_RESPONSES_EXTENSION_NAMESPACE)
.or_else(|| extensions.get(OPENAI_RESPONSES_LEGACY_EXTENSION_NAMESPACE))
.and_then(|value| value.get("item_type"))
.and_then(Value::as_str)?;
matches!(
item_type,
"custom_tool_call_output"
| "local_shell_call_output"
| "shell_call_output"
| "apply_patch_call_output"
| "computer_call_output"
)
.then_some(item_type)
}
pub(crate) fn ensure_modern_openai_responses_response_fields(
response: &mut Map<String, Value>,
) -> bool {
let mut changed = false;
if !response
.get("output")
.is_some_and(|value| matches!(value, Value::Array(_)))
{
response.insert("output".to_string(), Value::Array(Vec::new()));
changed = true;
}
if !response.contains_key("created_at") {
let created_at = response
.get("created")
.and_then(openai_responses_timestamp_value)
.unwrap_or_else(openai_responses_current_timestamp);
response.insert("created_at".to_string(), Value::from(created_at));
changed = true;
}
if response
.get("status")
.and_then(Value::as_str)
.is_none_or(|status| status == "completed")
&& !response.contains_key("completed_at")
{
let completed_at = response
.get("created_at")
.and_then(openai_responses_timestamp_value)
.unwrap_or_else(openai_responses_current_timestamp);
response.insert("completed_at".to_string(), Value::from(completed_at));
changed = true;
}
if !response.contains_key("output_text") {
let output_text = openai_responses_output_text_from_output(response.get("output"));
response.insert("output_text".to_string(), Value::String(output_text));
changed = true;
}
changed
}
pub(crate) fn openai_responses_output_text_from_output(output: Option<&Value>) -> String {
output
.and_then(Value::as_array)
.into_iter()
.flatten()
.filter_map(Value::as_object)
.flat_map(|item| {
item.get("content")
.and_then(Value::as_array)
.into_iter()
.flatten()
})
.filter_map(|part| {
let part = part.as_object()?;
matches!(
part.get("type").and_then(Value::as_str),
Some("output_text" | "text")
)
.then(|| part.get("text").and_then(Value::as_str).unwrap_or_default())
})
.collect::<String>()
}
pub(crate) fn openai_responses_current_timestamp() -> i64 {
SystemTime::now()
.duration_since(UNIX_EPOCH)
.map(|duration| duration.as_secs() as i64)
.unwrap_or_default()
}
fn openai_responses_timestamp_value(value: &Value) -> Option<i64> {
value
.as_i64()
.or_else(|| value.as_u64().and_then(|value| i64::try_from(value).ok()))
}
fn image_block_is_generation_call(extensions: &BTreeMap<String, Value>) -> bool {
extensions
.get(OPENAI_RESPONSES_EXTENSION_NAMESPACE)
.or_else(|| extensions.get(OPENAI_RESPONSES_LEGACY_EXTENSION_NAMESPACE))
.and_then(|value| value.get("item_type"))
.and_then(Value::as_str)
.is_some_and(|value| value == "image_generation_call")
}
fn openai_responses_image_generation_call_item(
response_id: &str,
index: usize,
data: &Option<String>,
url: &Option<String>,
media_type: &Option<String>,
) -> Value {
let mut item = Map::new();
item.insert(
"id".to_string(),
Value::String(format!("{response_id}_ig_{index}")),
);
item.insert(
"type".to_string(),
Value::String("image_generation_call".to_string()),
);
item.insert("status".to_string(), Value::String("completed".to_string()));
item.insert("action".to_string(), Value::String("generate".to_string()));
item.insert(
"output_format".to_string(),
Value::String(openai_responses_output_format_from_mime_type(
media_type.as_deref().unwrap_or("image/png"),
)),
);
if let Some(data) = data.as_ref().filter(|value| !value.trim().is_empty()) {
item.insert("result".to_string(), Value::String(data.clone()));
} else if let Some(url) = url.as_ref().filter(|value| !value.trim().is_empty()) {
item.insert("url".to_string(), Value::String(url.clone()));
} else {
item.insert("result".to_string(), Value::String(String::new()));
}
Value::Object(item)
}
fn openai_responses_output_format_from_mime_type(mime_type: &str) -> String {
match mime_type.trim().to_ascii_lowercase().as_str() {
"image/jpeg" | "image/jpg" => "jpeg",
"image/webp" => "webp",
"image/gif" => "gif",
_ => "png",
}
.to_string()
}
fn is_responses_web_search_tool(name: &str) -> bool {
matches!(name, "web_search" | "web_search_preview")
}
fn web_search_query_from_value(input: &Value) -> String {
input
.get("query")
.and_then(Value::as_str)
.or_else(|| input.as_str())
.unwrap_or_default()
.to_string()
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn responses_response_builder_emits_web_search_call_for_web_search_tool_use() {
let response = CanonicalResponse {
id: "resp_test".to_string(),
model: "gpt-5-5-low".to_string(),
content: vec![CanonicalContentBlock::ToolUse {
id: "call_ws_1".to_string(),
name: "web_search".to_string(),
input: json!({"query": "today tech"}),
extensions: BTreeMap::new(),
}],
outputs: Vec::new(),
stop_reason: Some(CanonicalStopReason::ToolUse),
usage: None,
extensions: BTreeMap::new(),
};
let body = to_raw(&response, &json!({}), false);
assert_eq!(body["output"][0]["type"], "web_search_call");
assert_eq!(body["output"][0]["id"], "call_ws_1");
assert_eq!(body["output"][0]["status"], "completed");
assert_eq!(body["output"][0]["action"]["type"], "search");
assert_eq!(body["output"][0]["action"]["query"], "today tech");
assert_eq!(body["output_text"], "");
assert!(body["created_at"].as_i64().is_some());
assert!(body["completed_at"].as_i64().is_some());
}
#[test]
fn responses_response_builder_emits_modern_output_text_and_preserves_source_fields() {
let mut extensions = BTreeMap::new();
extensions.insert(
OPENAI_RESPONSES_EXTENSION_NAMESPACE.to_string(),
json!({
"created_at": 111,
"completed_at": 222,
"output_text": "source text",
"conversation": {"id": "conv_123"}
}),
);
let response = CanonicalResponse {
id: "resp_text".to_string(),
model: "gpt-5".to_string(),
content: vec![CanonicalContentBlock::Text {
text: "generated text".to_string(),
extensions: BTreeMap::new(),
}],
outputs: Vec::new(),
stop_reason: Some(CanonicalStopReason::EndTurn),
usage: None,
extensions,
};
let body = to_raw(&response, &json!({}), false);
assert_eq!(body["output_text"], "source text");
assert_eq!(body["created_at"], 111);
assert_eq!(body["completed_at"], 222);
assert_eq!(body["conversation"]["id"], "conv_123");
}
#[test]
fn responses_response_builder_encodes_tool_errors_in_output() {
let response = CanonicalResponse {
id: "resp_tool_error".to_string(),
model: "gpt-5.6-sol".to_string(),
content: vec![CanonicalContentBlock::ToolResult {
tool_use_id: "call_error".to_string(),
name: None,
output: Some(json!("command failed")),
content_text: None,
is_error: true,
extensions: BTreeMap::new(),
}],
outputs: Vec::new(),
stop_reason: Some(CanonicalStopReason::EndTurn),
usage: None,
extensions: BTreeMap::new(),
};
let body = to_raw(&response, &json!({}), false);
let item = &body["output"][0];
assert_eq!(item["type"], "function_call_output");
assert_eq!(item["call_id"], "call_error");
assert_eq!(item["output"], "[tool error]\ncommand failed");
assert!(item.get("is_error").is_none());
}
#[test]
fn compact_response_builder_emits_the_compaction_resource_shape() {
let mut extensions = BTreeMap::new();
extensions.insert(
OPENAI_RESPONSES_EXTENSION_NAMESPACE.to_string(),
json!({"created_at": 123}),
);
let response = CanonicalResponse {
id: "resp_compact".to_string(),
model: "gpt-5.6-sol".to_string(),
content: vec![CanonicalContentBlock::Text {
text: "compacted context".to_string(),
extensions: BTreeMap::new(),
}],
outputs: Vec::new(),
stop_reason: Some(CanonicalStopReason::EndTurn),
usage: None,
extensions,
};
let body = to_raw(&response, &json!({}), true);
let keys = body
.as_object()
.expect("Compact response should be an object")
.keys()
.map(String::as_str)
.collect::<std::collections::BTreeSet<_>>();
assert_eq!(body["object"], "response.compaction");
assert_eq!(body["created_at"], 123);
assert_eq!(
keys,
std::collections::BTreeSet::from(["created_at", "id", "object", "output"])
);
}
#[test]
fn responses_response_parser_preserves_encrypted_reasoning_without_summary() {
let body = json!({
"id": "resp_test",
"model": "gpt-5",
"status": "completed",
"output": [{
"type": "reasoning",
"id": "rs_1",
"status": "completed",
"summary": [],
"encrypted_content": "openai-opaque"
}]
});
let canonical = from_raw(&body).expect("response should parse");
assert!(matches!(
canonical.content.first(),
Some(CanonicalContentBlock::Thinking {
text,
encrypted_content,
..
}) if text.is_empty() && encrypted_content.as_deref() == Some("openai-opaque")
));
let rebuilt = to_raw(&canonical, &json!({}), false);
assert_eq!(rebuilt["output"][0]["type"], "reasoning");
assert_eq!(
rebuilt["output"][0]["encrypted_content"],
json!("openai-opaque")
);
}
#[test]
fn responses_response_builder_does_not_emit_claude_redacted_as_openai_encrypted_content() {
let mut extensions = BTreeMap::new();
extensions.insert("aether".to_string(), json!({"source": "claude_thinking"}));
let response = CanonicalResponse {
id: "msg_claude".to_string(),
model: "claude-sonnet".to_string(),
content: vec![CanonicalContentBlock::Thinking {
text: String::new(),
signature: None,
encrypted_content: Some("{\"type\":\"redacted_thinking\",\"v\":5}".to_string()),
extensions,
}],
outputs: Vec::new(),
stop_reason: Some(CanonicalStopReason::EndTurn),
usage: None,
extensions: BTreeMap::new(),
};
let body = to_raw(&response, &json!({}), false);
assert!(body["output"].as_array().expect("output").is_empty());
assert!(!body.to_string().contains("encrypted_content"));
}
#[test]
fn responses_response_parser_reads_web_search_call_as_tool_use() {
let body = json!({
"id": "resp_test",
"model": "gpt-5-5-low",
"status": "incomplete",
"output": [{
"type": "web_search_call",
"id": "call_ws_1",
"status": "completed",
"action": {"type": "search", "query": "today tech"}
}]
});
let canonical = from_raw(&body).expect("response should parse");
assert!(
matches!(canonical.content.first(), Some(CanonicalContentBlock::ToolUse {
id,
name,
input,
..
}) if id == "call_ws_1" && name == "web_search" && input["query"] == "today tech")
);
}
}
@@ -0,0 +1,66 @@
use crate::contracts::{
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,
}),
_ => 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 does_not_resolve_openai_responses_compact_as_streaming() {
assert!(resolve_stream_spec("openai_responses_compact_stream").is_none());
}
}
@@ -0,0 +1,3 @@
pub use crate::formats::openai::chat::stream::{
OpenAIResponsesClientEmitter, OpenAIResponsesProviderState,
};
@@ -0,0 +1,219 @@
use serde_json::Value;
const REQUEST_FIELDS: &[&str] = &[
"id",
"model",
"reasoning",
"input",
"commands",
"settings",
"max_output_tokens",
];
const REASONING_FIELDS: &[&str] = &["effort", "summary", "context"];
const COMMAND_FIELDS: &[&str] = &[
"search_query",
"image_query",
"open",
"click",
"find",
"screenshot",
"finance",
"weather",
"sports",
"time",
"response_length",
];
const COMMAND_ITEM_FIELDS: &[(&str, &[&str])] = &[
("search_query", &["q", "recency", "domains"]),
("image_query", &["q", "recency", "domains"]),
("open", &["ref_id", "lineno"]),
("click", &["ref_id", "id"]),
("find", &["ref_id", "pattern"]),
("screenshot", &["ref_id", "pageno"]),
("finance", &["ticker", "type", "market"]),
("weather", &["location", "start", "duration"]),
(
"sports",
&[
"tool",
"fn",
"league",
"team",
"opponent",
"date_from",
"date_to",
"num_games",
"locale",
],
),
("time", &["utc_offset"]),
];
const SETTINGS_FIELDS: &[&str] = &[
"user_location",
"search_context_size",
"filters",
"image_settings",
"allowed_callers",
"external_web_access",
];
const USER_LOCATION_FIELDS: &[&str] = &["type", "country", "region", "city", "timezone"];
const FILTER_FIELDS: &[&str] = &["allowed_domains", "blocked_domains"];
const IMAGE_SETTINGS_FIELDS: &[&str] = &["max_results", "caption"];
fn retain_object_fields(value: &mut Value, fields: &[&str]) {
if let Some(object) = value.as_object_mut() {
object.retain(|field, _| fields.contains(&field.as_str()));
}
}
fn retain_array_object_fields(
object: &mut serde_json::Map<String, Value>,
key: &str,
fields: &[&str],
) {
if let Some(items) = object.get_mut(key).and_then(Value::as_array_mut) {
for item in items {
retain_object_fields(item, fields);
}
}
}
pub fn apply_openai_search_request_projection(body: &mut Value, provider_api_format: &str) {
if !crate::api_format_alias_matches(provider_api_format, "openai:search") {
return;
}
let Some(body_object) = body.as_object_mut() else {
return;
};
body_object.retain(|field, _| REQUEST_FIELDS.contains(&field.as_str()));
if let Some(reasoning) = body_object.get_mut("reasoning") {
retain_object_fields(reasoning, REASONING_FIELDS);
}
if let Some(commands) = body_object
.get_mut("commands")
.and_then(Value::as_object_mut)
{
commands.retain(|field, _| COMMAND_FIELDS.contains(&field.as_str()));
for (key, fields) in COMMAND_ITEM_FIELDS {
retain_array_object_fields(commands, key, fields);
}
}
if let Some(settings) = body_object
.get_mut("settings")
.and_then(Value::as_object_mut)
{
settings.retain(|field, _| SETTINGS_FIELDS.contains(&field.as_str()));
if let Some(user_location) = settings.get_mut("user_location") {
retain_object_fields(user_location, USER_LOCATION_FIELDS);
}
if let Some(filters) = settings.get_mut("filters") {
retain_object_fields(filters, FILTER_FIELDS);
}
if let Some(image_settings) = settings.get_mut("image_settings") {
retain_object_fields(image_settings, IMAGE_SETTINGS_FIELDS);
}
}
}
#[cfg(test)]
mod tests {
use serde_json::json;
use super::apply_openai_search_request_projection;
#[test]
fn projects_the_typed_search_request_contract() {
let mut body = json!({
"id": "session-1",
"model": "gpt-5.6-sol",
"reasoning": {
"effort": "max",
"summary": "auto",
"context": "current_turn",
"mode": "pro"
},
"input": "find documentation",
"commands": {
"search_query": [{"q": "Aether", "recency": 7, "unknown": true}],
"image_query": [{"q": "Aether UI", "domains": ["example.com"], "unknown": true}],
"open": [{"ref_id": "turn0search0", "lineno": 12, "unknown": true}],
"click": [{"ref_id": "turn0fetch0", "id": 3, "unknown": true}],
"find": [{"ref_id": "turn0fetch0", "pattern": "Aether", "unknown": true}],
"screenshot": [{"ref_id": "turn0fetch0", "pageno": 2, "unknown": true}],
"finance": [{"ticker": "OPENAI", "type": "equity", "market": "USA", "unknown": true}],
"weather": [{"location": "US, CA, San Francisco", "duration": 3, "unknown": true}],
"sports": [{"tool": "sports", "fn": "schedule", "league": "nba", "team": "GSW", "unknown": true}],
"time": [{"utc_offset": "+08:00", "unknown": true}],
"response_length": "short",
"unknown": true
},
"settings": {
"user_location": {"type": "approximate", "country": "US", "unknown": true},
"search_context_size": "high",
"filters": {"allowed_domains": ["openai.com"], "unknown": true},
"image_settings": {"max_results": 3, "unknown": true},
"allowed_callers": ["direct"],
"external_web_access": "live",
"unknown": true
},
"max_output_tokens": 1024,
"store": false,
"stream": true,
"service_tier": "priority",
"unknown": true
});
apply_openai_search_request_projection(&mut body, "/v1/alpha/search");
assert_eq!(
body["reasoning"],
json!({
"effort": "max",
"summary": "auto",
"context": "current_turn"
})
);
assert_eq!(
body["commands"],
json!({
"search_query": [{"q": "Aether", "recency": 7}],
"image_query": [{"q": "Aether UI", "domains": ["example.com"]}],
"open": [{"ref_id": "turn0search0", "lineno": 12}],
"click": [{"ref_id": "turn0fetch0", "id": 3}],
"find": [{"ref_id": "turn0fetch0", "pattern": "Aether"}],
"screenshot": [{"ref_id": "turn0fetch0", "pageno": 2}],
"finance": [{"ticker": "OPENAI", "type": "equity", "market": "USA"}],
"weather": [{"location": "US, CA, San Francisco", "duration": 3}],
"sports": [{"tool": "sports", "fn": "schedule", "league": "nba", "team": "GSW"}],
"time": [{"utc_offset": "+08:00"}],
"response_length": "short"
})
);
assert_eq!(
body["settings"],
json!({
"user_location": {"type": "approximate", "country": "US"},
"search_context_size": "high",
"filters": {"allowed_domains": ["openai.com"]},
"image_settings": {"max_results": 3},
"allowed_callers": ["direct"],
"external_web_access": "live"
})
);
assert!(body.get("store").is_none());
assert!(body.get("stream").is_none());
assert!(body.get("service_tier").is_none());
assert!(body.get("unknown").is_none());
}
#[test]
fn leaves_other_formats_unchanged() {
let mut body = json!({"model": "gpt-5.6-sol", "store": true});
let expected = body.clone();
apply_openai_search_request_projection(&mut body, "openai:responses");
assert_eq!(body, expected);
}
}
@@ -0,0 +1,198 @@
use serde_json::{Map, Value};
use crate::formats::shared::model_directives::ReasoningEffort;
macro_rules! define_openai_reasoning_effort {
($name:ident) => {
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum $name {
None,
Minimal,
Low,
Medium,
High,
XHigh,
Max,
}
impl $name {
pub fn parse(value: &str) -> Option<Self> {
match value.trim().to_ascii_lowercase().as_str() {
"none" => Some(Self::None),
"minimal" => Some(Self::Minimal),
"low" => Some(Self::Low),
"medium" => Some(Self::Medium),
"high" => Some(Self::High),
"xhigh" => Some(Self::XHigh),
"max" => Some(Self::Max),
_ => None,
}
}
pub fn as_str(self) -> &'static str {
match self {
Self::None => "none",
Self::Minimal => "minimal",
Self::Low => "low",
Self::Medium => "medium",
Self::High => "high",
Self::XHigh => "xhigh",
Self::Max => "max",
}
}
}
};
}
define_openai_reasoning_effort!(OpenAiChatReasoningEffort);
define_openai_reasoning_effort!(OpenAiResponsesReasoningEffort);
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum OpenAiPromptCacheRetention {
InMemory,
TwentyFourHours,
}
impl OpenAiPromptCacheRetention {
pub fn parse(value: &str) -> Option<Self> {
match value.trim() {
"in_memory" => Some(Self::InMemory),
"24h" => Some(Self::TwentyFourHours),
_ => None,
}
}
pub fn as_str(self) -> &'static str {
match self {
Self::InMemory => "in_memory",
Self::TwentyFourHours => "24h",
}
}
}
#[deprecated(note = "use OpenAiChatReasoningEffort or OpenAiResponsesReasoningEffort")]
pub type OpenAiReasoningEffort = OpenAiChatReasoningEffort;
pub fn parse_openai_stop_sequences(stop: Option<&Value>) -> Option<Vec<Value>> {
match stop {
Some(Value::String(value)) if !value.trim().is_empty() => {
Some(vec![Value::String(value.clone())])
}
Some(Value::Array(values)) => Some(
values
.iter()
.filter_map(|value| value.as_str())
.map(str::trim)
.filter(|value| !value.is_empty())
.map(|value| Value::String(value.to_string()))
.collect::<Vec<_>>(),
)
.filter(|values| !values.is_empty()),
_ => None,
}
}
pub fn resolve_openai_chat_max_tokens(request: &Map<String, Value>) -> u64 {
request
.get("max_completion_tokens")
.and_then(value_as_u64)
.or_else(|| request.get("max_tokens").and_then(value_as_u64))
.unwrap_or(4096)
}
pub fn value_as_u64(value: &Value) -> Option<u64> {
value
.as_u64()
.or_else(|| value.as_i64().and_then(|value| u64::try_from(value).ok()))
}
pub fn copy_request_number_field(
request: &Map<String, Value>,
target: &mut Map<String, Value>,
key: &str,
) {
copy_request_number_field_as(request, target, key, key);
}
pub fn copy_request_number_field_as(
request: &Map<String, Value>,
target: &mut Map<String, Value>,
source_key: &str,
target_key: &str,
) {
if let Some(value) = request.get(source_key).cloned() {
if value.is_number() {
target.insert(target_key.to_string(), value);
}
}
}
pub fn map_openai_reasoning_effort_to_claude_output(value: &str) -> Option<&'static str> {
ReasoningEffort::parse(value).map(ReasoningEffort::as_claude_output_value)
}
pub fn map_openai_reasoning_effort_to_thinking_budget(value: &str) -> Option<u64> {
ReasoningEffort::parse(value).map(ReasoningEffort::thinking_budget_tokens)
}
pub fn map_openai_reasoning_effort_to_gemini_budget(value: &str) -> Option<u64> {
map_openai_reasoning_effort_to_thinking_budget(value)
}
pub fn map_thinking_budget_to_openai_reasoning_effort(value: u64) -> &'static str {
match value {
0..=1664 => "low",
1665..=3072 => "medium",
3073..=6144 => "high",
_ => "xhigh",
}
}
pub fn extract_openai_reasoning_effort(request: &Map<String, Value>) -> Option<String> {
request
.get("reasoning_effort")
.and_then(Value::as_str)
.or_else(|| {
request
.get("reasoning")
.and_then(Value::as_object)
.and_then(|reasoning| reasoning.get("effort"))
.and_then(Value::as_str)
})
.map(str::trim)
.filter(|value| !value.is_empty())
.map(|value| value.to_ascii_lowercase())
}
#[cfg(test)]
mod tests {
use super::{
OpenAiChatReasoningEffort, OpenAiPromptCacheRetention, OpenAiResponsesReasoningEffort,
};
#[test]
fn openai_reasoning_effort_contract_includes_gpt_5_6_max() {
assert_eq!(
OpenAiChatReasoningEffort::parse("max").map(OpenAiChatReasoningEffort::as_str),
Some("max")
);
assert_eq!(
OpenAiResponsesReasoningEffort::parse("max")
.map(OpenAiResponsesReasoningEffort::as_str),
Some("max")
);
}
#[test]
fn prompt_cache_retention_uses_the_openai_wire_enum() {
assert_eq!(
OpenAiPromptCacheRetention::parse("in_memory").map(OpenAiPromptCacheRetention::as_str),
Some("in_memory")
);
assert_eq!(
OpenAiPromptCacheRetention::parse("24h").map(OpenAiPromptCacheRetention::as_str),
Some("24h")
);
assert_eq!(OpenAiPromptCacheRetention::parse("in-memory"), None);
}
}
@@ -0,0 +1 @@
pub mod spec;
@@ -0,0 +1,27 @@
use crate::contracts::OPENAI_VIDEO_CREATE_SYNC_PLAN_KIND;
use crate::formats::shared::video::{LocalVideoCreateFamily, LocalVideoCreateSpec};
pub fn resolve_sync_spec(plan_kind: &str) -> Option<LocalVideoCreateSpec> {
match plan_kind {
OPENAI_VIDEO_CREATE_SYNC_PLAN_KIND => Some(LocalVideoCreateSpec {
api_format: "openai:video",
decision_kind: OPENAI_VIDEO_CREATE_SYNC_PLAN_KIND,
report_kind: "openai_video_create_sync_finalize",
family: LocalVideoCreateFamily::OpenAi,
}),
_ => None,
}
}
#[cfg(test)]
mod tests {
use super::{resolve_sync_spec, LocalVideoCreateFamily};
#[test]
fn resolves_openai_video_create_spec() {
let spec = resolve_sync_spec("openai_video_create_sync").expect("spec");
assert_eq!(spec.api_format, "openai:video");
assert_eq!(spec.family, LocalVideoCreateFamily::OpenAi);
assert_eq!(spec.report_kind, "openai_video_create_sync_finalize");
}
}
File diff suppressed because it is too large Load Diff
@@ -0,0 +1,187 @@
use serde_json::{Map, Value};
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
pub enum LocalCoreSyncErrorKind {
InvalidRequest,
Authentication,
PermissionDenied,
NotFound,
RateLimit,
ContextLengthExceeded,
Overloaded,
ServerError,
}
pub fn is_core_error_finalize_kind(report_kind: &str) -> bool {
core_error_default_client_api_format(report_kind).is_some()
}
pub fn core_error_default_client_api_format(report_kind: &str) -> Option<&'static str> {
crate::contracts::core_error_default_client_api_format(report_kind)
}
pub fn core_error_background_report_kind(report_kind: &str) -> Option<&'static str> {
crate::contracts::core_error_background_report_kind(report_kind)
}
pub fn core_success_background_report_kind(report_kind: &str) -> Option<&'static str> {
crate::contracts::core_success_background_report_kind(report_kind)
}
pub fn build_core_error_body_for_client_format(
client_api_format: &str,
message: &str,
code: Option<&str>,
kind: LocalCoreSyncErrorKind,
) -> Option<Value> {
let mut error_object = Map::new();
error_object.insert("message".to_string(), Value::String(message.to_string()));
match aether_ai_formats::normalize_api_format_alias(client_api_format).as_str() {
"openai:chat"
| "openai:responses"
| "openai:responses:compact"
| "openai:search"
| "openai:embedding" => {
error_object.insert(
"type".to_string(),
Value::String(map_local_sync_error_kind_to_openai_type(kind).to_string()),
);
if let Some(code) = code.filter(|value| !value.is_empty()) {
error_object.insert("code".to_string(), Value::String(code.to_string()));
}
Some(Value::Object(Map::from_iter([(
"error".to_string(),
Value::Object(error_object),
)])))
}
"claude:messages" => {
error_object.insert(
"type".to_string(),
Value::String(map_local_sync_error_kind_to_claude_type(kind).to_string()),
);
if let Some(code) = code.filter(|value| !value.is_empty()) {
error_object.insert("code".to_string(), Value::String(code.to_string()));
}
Some(Value::Object(Map::from_iter([
("type".to_string(), Value::String("error".to_string())),
("error".to_string(), Value::Object(error_object)),
])))
}
"gemini:generate_content" => Some(Value::Object(Map::from_iter([(
"error".to_string(),
Value::Object(Map::from_iter([
(
"code".to_string(),
Value::from(map_local_sync_error_kind_to_gemini_code(kind)),
),
("message".to_string(), Value::String(message.to_string())),
(
"status".to_string(),
Value::String(map_local_sync_error_kind_to_gemini_status(kind).to_string()),
),
])),
)]))),
_ => None,
}
}
fn map_local_sync_error_kind_to_openai_type(kind: LocalCoreSyncErrorKind) -> &'static str {
match kind {
LocalCoreSyncErrorKind::InvalidRequest => "invalid_request_error",
LocalCoreSyncErrorKind::Authentication => "authentication_error",
LocalCoreSyncErrorKind::PermissionDenied => "permission_error",
LocalCoreSyncErrorKind::NotFound => "not_found_error",
LocalCoreSyncErrorKind::RateLimit => "rate_limit_error",
LocalCoreSyncErrorKind::ContextLengthExceeded => "context_length_exceeded",
LocalCoreSyncErrorKind::Overloaded | LocalCoreSyncErrorKind::ServerError => "server_error",
}
}
fn map_local_sync_error_kind_to_claude_type(kind: LocalCoreSyncErrorKind) -> &'static str {
match kind {
LocalCoreSyncErrorKind::InvalidRequest | LocalCoreSyncErrorKind::ContextLengthExceeded => {
"invalid_request_error"
}
LocalCoreSyncErrorKind::Authentication => "authentication_error",
LocalCoreSyncErrorKind::PermissionDenied => "permission_error",
LocalCoreSyncErrorKind::NotFound => "not_found_error",
LocalCoreSyncErrorKind::RateLimit => "rate_limit_error",
LocalCoreSyncErrorKind::Overloaded | LocalCoreSyncErrorKind::ServerError => "api_error",
}
}
fn map_local_sync_error_kind_to_gemini_code(kind: LocalCoreSyncErrorKind) -> u16 {
match kind {
LocalCoreSyncErrorKind::InvalidRequest | LocalCoreSyncErrorKind::ContextLengthExceeded => {
400
}
LocalCoreSyncErrorKind::Authentication => 401,
LocalCoreSyncErrorKind::PermissionDenied => 403,
LocalCoreSyncErrorKind::NotFound => 404,
LocalCoreSyncErrorKind::RateLimit => 429,
LocalCoreSyncErrorKind::Overloaded => 503,
LocalCoreSyncErrorKind::ServerError => 500,
}
}
fn map_local_sync_error_kind_to_gemini_status(kind: LocalCoreSyncErrorKind) -> &'static str {
match kind {
LocalCoreSyncErrorKind::InvalidRequest | LocalCoreSyncErrorKind::ContextLengthExceeded => {
"INVALID_ARGUMENT"
}
LocalCoreSyncErrorKind::Authentication => "UNAUTHENTICATED",
LocalCoreSyncErrorKind::PermissionDenied => "PERMISSION_DENIED",
LocalCoreSyncErrorKind::NotFound => "NOT_FOUND",
LocalCoreSyncErrorKind::RateLimit => "RESOURCE_EXHAUSTED",
LocalCoreSyncErrorKind::Overloaded => "UNAVAILABLE",
LocalCoreSyncErrorKind::ServerError => "INTERNAL",
}
}
#[cfg(test)]
mod tests {
use super::{
build_core_error_body_for_client_format, core_success_background_report_kind,
is_core_error_finalize_kind, LocalCoreSyncErrorKind,
};
#[test]
fn builds_openai_core_error_body() {
let body = build_core_error_body_for_client_format(
"openai:chat",
"bad request",
Some("invalid_request"),
LocalCoreSyncErrorKind::InvalidRequest,
)
.expect("body should build");
assert_eq!(body["error"]["message"], "bad request");
assert_eq!(body["error"]["type"], "invalid_request_error");
assert_eq!(body["error"]["code"], "invalid_request");
}
#[test]
fn builds_openai_search_core_error_body() {
let body = build_core_error_body_for_client_format(
"openai:search",
"search unavailable",
Some("upstream_unavailable"),
LocalCoreSyncErrorKind::ServerError,
)
.expect("body should build");
assert_eq!(body["error"]["message"], "search unavailable");
assert_eq!(body["error"]["type"], "server_error");
assert_eq!(body["error"]["code"], "upstream_unavailable");
}
#[test]
fn recognizes_finalize_kind_and_success_mapping() {
assert!(is_core_error_finalize_kind("openai_chat_sync_finalize"));
assert_eq!(
core_success_background_report_kind("openai_chat_sync_finalize"),
Some("openai_chat_sync_success")
);
}
}
@@ -0,0 +1,22 @@
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum LocalStandardSourceFamily {
Standard,
Gemini,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum LocalStandardSourceMode {
Chat,
Cli,
Embedding,
}
#[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,
}
File diff suppressed because it is too large Load Diff
@@ -0,0 +1,55 @@
use std::fmt;
pub mod error_body;
pub mod family;
pub mod image_bridge;
pub mod model_directives;
pub mod passthrough;
pub mod request;
pub mod request_matrix;
pub mod response;
pub mod routing;
pub mod sse;
pub mod standard_matrix;
pub mod standard_normalize;
pub mod stream_core;
pub mod stream_rewrite;
pub mod sync_products;
pub mod sync_to_stream;
pub mod video;
pub use self::sse::{encode_done_sse, encode_json_sse, map_claude_stop_reason};
pub use self::stream_core::{CanonicalStreamEvent, CanonicalStreamFrame};
pub use self::stream_rewrite::{
maybe_build_ai_surface_stream_rewriter, resolve_finalize_stream_rewrite_mode,
AiSurfaceStreamRewriter, FinalizeStreamRewriteMode,
};
#[derive(Debug)]
pub struct AiSurfaceFinalizeError(pub String);
impl AiSurfaceFinalizeError {
pub fn new(message: impl Into<String>) -> Self {
Self(message.into())
}
}
impl fmt::Display for AiSurfaceFinalizeError {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(f, "AI surface finalize error: {}", self.0)
}
}
impl std::error::Error for AiSurfaceFinalizeError {}
impl From<serde_json::Error> for AiSurfaceFinalizeError {
fn from(source: serde_json::Error) -> Self {
Self(source.to_string())
}
}
impl From<base64::DecodeError> for AiSurfaceFinalizeError {
fn from(source: base64::DecodeError) -> Self {
Self(source.to_string())
}
}
File diff suppressed because it is too large Load Diff
@@ -0,0 +1,204 @@
use crate::contracts::{
CLAUDE_CHAT_STREAM_PLAN_KIND, CLAUDE_CHAT_SYNC_PLAN_KIND, CLAUDE_CLI_STREAM_PLAN_KIND,
CLAUDE_CLI_SYNC_PLAN_KIND, GEMINI_CHAT_STREAM_PLAN_KIND, GEMINI_CHAT_SYNC_PLAN_KIND,
GEMINI_CLI_STREAM_PLAN_KIND, GEMINI_CLI_SYNC_PLAN_KIND, GEMINI_EMBEDDING_SYNC_PLAN_KIND,
GEMINI_EMBEDDING_SYNC_SUCCESS_REPORT_KIND, GEMINI_INTERACTIONS_STREAM_PLAN_KIND,
GEMINI_INTERACTIONS_STREAM_SUCCESS_REPORT_KIND, GEMINI_INTERACTIONS_SYNC_PLAN_KIND,
GEMINI_INTERACTIONS_SYNC_SUCCESS_REPORT_KIND, OPENAI_EMBEDDING_SYNC_PLAN_KIND,
OPENAI_RERANK_SYNC_PLAN_KIND, OPENAI_SEARCH_SYNC_PLAN_KIND,
OPENAI_SEARCH_SYNC_SUCCESS_REPORT_KIND,
};
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum LocalSameFormatProviderFamily {
Standard,
Gemini,
}
#[derive(Debug, Clone, Copy)]
pub struct LocalSameFormatProviderSpec {
pub api_format: &'static str,
pub decision_kind: &'static str,
pub report_kind: &'static str,
pub family: LocalSameFormatProviderFamily,
pub require_streaming: bool,
}
pub fn resolve_sync_spec(plan_kind: &str) -> Option<LocalSameFormatProviderSpec> {
match plan_kind {
CLAUDE_CHAT_SYNC_PLAN_KIND => Some(LocalSameFormatProviderSpec {
api_format: "claude:messages",
decision_kind: CLAUDE_CHAT_SYNC_PLAN_KIND,
report_kind: "claude_chat_sync_success",
family: LocalSameFormatProviderFamily::Standard,
require_streaming: false,
}),
CLAUDE_CLI_SYNC_PLAN_KIND => Some(LocalSameFormatProviderSpec {
api_format: "claude:messages",
decision_kind: CLAUDE_CLI_SYNC_PLAN_KIND,
report_kind: "claude_cli_sync_success",
family: LocalSameFormatProviderFamily::Standard,
require_streaming: false,
}),
GEMINI_CHAT_SYNC_PLAN_KIND => Some(LocalSameFormatProviderSpec {
api_format: "gemini:generate_content",
decision_kind: GEMINI_CHAT_SYNC_PLAN_KIND,
report_kind: "gemini_chat_sync_success",
family: LocalSameFormatProviderFamily::Gemini,
require_streaming: false,
}),
GEMINI_CLI_SYNC_PLAN_KIND => Some(LocalSameFormatProviderSpec {
api_format: "gemini:generate_content",
decision_kind: GEMINI_CLI_SYNC_PLAN_KIND,
report_kind: "gemini_cli_sync_success",
family: LocalSameFormatProviderFamily::Gemini,
require_streaming: false,
}),
GEMINI_EMBEDDING_SYNC_PLAN_KIND => Some(LocalSameFormatProviderSpec {
api_format: "gemini:embedding",
decision_kind: GEMINI_EMBEDDING_SYNC_PLAN_KIND,
report_kind: GEMINI_EMBEDDING_SYNC_SUCCESS_REPORT_KIND,
family: LocalSameFormatProviderFamily::Gemini,
require_streaming: false,
}),
GEMINI_INTERACTIONS_SYNC_PLAN_KIND => Some(LocalSameFormatProviderSpec {
api_format: "gemini:interactions",
decision_kind: GEMINI_INTERACTIONS_SYNC_PLAN_KIND,
report_kind: GEMINI_INTERACTIONS_SYNC_SUCCESS_REPORT_KIND,
family: LocalSameFormatProviderFamily::Gemini,
require_streaming: false,
}),
OPENAI_EMBEDDING_SYNC_PLAN_KIND => Some(LocalSameFormatProviderSpec {
api_format: "openai:embedding",
decision_kind: OPENAI_EMBEDDING_SYNC_PLAN_KIND,
report_kind: "openai_embedding_sync_success",
family: LocalSameFormatProviderFamily::Standard,
require_streaming: false,
}),
OPENAI_RERANK_SYNC_PLAN_KIND => Some(LocalSameFormatProviderSpec {
api_format: "openai:rerank",
decision_kind: OPENAI_RERANK_SYNC_PLAN_KIND,
report_kind: "openai_rerank_sync_success",
family: LocalSameFormatProviderFamily::Standard,
require_streaming: false,
}),
OPENAI_SEARCH_SYNC_PLAN_KIND => Some(LocalSameFormatProviderSpec {
api_format: "openai:search",
decision_kind: OPENAI_SEARCH_SYNC_PLAN_KIND,
report_kind: OPENAI_SEARCH_SYNC_SUCCESS_REPORT_KIND,
family: LocalSameFormatProviderFamily::Standard,
require_streaming: false,
}),
_ => None,
}
}
pub fn resolve_stream_spec(plan_kind: &str) -> Option<LocalSameFormatProviderSpec> {
match plan_kind {
CLAUDE_CHAT_STREAM_PLAN_KIND => Some(LocalSameFormatProviderSpec {
api_format: "claude:messages",
decision_kind: CLAUDE_CHAT_STREAM_PLAN_KIND,
report_kind: "claude_chat_stream_success",
family: LocalSameFormatProviderFamily::Standard,
require_streaming: true,
}),
CLAUDE_CLI_STREAM_PLAN_KIND => Some(LocalSameFormatProviderSpec {
api_format: "claude:messages",
decision_kind: CLAUDE_CLI_STREAM_PLAN_KIND,
report_kind: "claude_cli_stream_success",
family: LocalSameFormatProviderFamily::Standard,
require_streaming: true,
}),
GEMINI_CHAT_STREAM_PLAN_KIND => Some(LocalSameFormatProviderSpec {
api_format: "gemini:generate_content",
decision_kind: GEMINI_CHAT_STREAM_PLAN_KIND,
report_kind: "gemini_chat_stream_success",
family: LocalSameFormatProviderFamily::Gemini,
require_streaming: true,
}),
GEMINI_CLI_STREAM_PLAN_KIND => Some(LocalSameFormatProviderSpec {
api_format: "gemini:generate_content",
decision_kind: GEMINI_CLI_STREAM_PLAN_KIND,
report_kind: "gemini_cli_stream_success",
family: LocalSameFormatProviderFamily::Gemini,
require_streaming: true,
}),
GEMINI_INTERACTIONS_STREAM_PLAN_KIND => Some(LocalSameFormatProviderSpec {
api_format: "gemini:interactions",
decision_kind: GEMINI_INTERACTIONS_STREAM_PLAN_KIND,
report_kind: GEMINI_INTERACTIONS_STREAM_SUCCESS_REPORT_KIND,
family: LocalSameFormatProviderFamily::Gemini,
require_streaming: true,
}),
_ => None,
}
}
#[cfg(test)]
mod tests {
use super::{resolve_stream_spec, resolve_sync_spec};
#[test]
fn resolves_claude_sync_same_format_spec() {
let spec = resolve_sync_spec("claude_chat_sync").expect("spec");
assert_eq!(spec.api_format, "claude:messages");
assert_eq!(spec.report_kind, "claude_chat_sync_success");
assert!(!spec.require_streaming);
}
#[test]
fn resolves_gemini_stream_same_format_spec() {
let spec = resolve_stream_spec("gemini_cli_stream").expect("spec");
assert_eq!(spec.api_format, "gemini:generate_content");
assert_eq!(spec.report_kind, "gemini_cli_stream_success");
assert!(spec.require_streaming);
}
#[test]
fn resolves_openai_embedding_sync_same_format_spec() {
let spec = resolve_sync_spec("openai_embedding_sync").expect("spec");
assert_eq!(spec.api_format, "openai:embedding");
assert_eq!(spec.report_kind, "openai_embedding_sync_success");
assert!(!spec.require_streaming);
}
#[test]
fn resolves_gemini_embedding_sync_same_format_spec() {
let spec = resolve_sync_spec("gemini_embedding_sync").expect("spec");
assert_eq!(spec.api_format, "gemini:embedding");
assert_eq!(spec.report_kind, "gemini_embedding_sync_success");
assert_eq!(spec.family, super::LocalSameFormatProviderFamily::Gemini);
assert!(!spec.require_streaming);
}
#[test]
fn resolves_gemini_interactions_same_format_specs() {
let sync = resolve_sync_spec("gemini_interactions_sync").expect("sync spec");
assert_eq!(sync.api_format, "gemini:interactions");
assert_eq!(sync.report_kind, "gemini_interactions_sync_success");
assert_eq!(sync.family, super::LocalSameFormatProviderFamily::Gemini);
assert!(!sync.require_streaming);
let stream = resolve_stream_spec("gemini_interactions_stream").expect("stream spec");
assert_eq!(stream.api_format, "gemini:interactions");
assert_eq!(stream.report_kind, "gemini_interactions_stream_success");
assert_eq!(stream.family, super::LocalSameFormatProviderFamily::Gemini);
assert!(stream.require_streaming);
}
#[test]
fn resolves_openai_rerank_sync_same_format_spec() {
let spec = resolve_sync_spec("openai_rerank_sync").expect("spec");
assert_eq!(spec.api_format, "openai:rerank");
assert_eq!(spec.report_kind, "openai_rerank_sync_success");
assert!(!spec.require_streaming);
}
#[test]
fn resolves_openai_search_sync_spec() {
let spec = resolve_sync_spec("openai_search_sync").expect("spec");
assert_eq!(spec.api_format, "openai:search");
assert_eq!(spec.report_kind, "openai_search_sync_success");
assert!(!spec.require_streaming);
}
}
@@ -0,0 +1,502 @@
use base64::Engine as _;
use crate::formats::id::api_format_uses_body_stream_field;
/// JSON key under which `upstream_is_stream` is written into the AI execution
/// report context and propagated into usage metadata. Shared by the producer
/// (`aether-ai-serving::report_context`) and every downstream consumer so that
/// renames cannot silently desync them — a string-literal mismatch here would
/// degrade to default values (e.g. assuming streaming) without any compile-time
/// signal.
pub const UPSTREAM_IS_STREAM_KEY: &str = "upstream_is_stream";
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub(crate) enum UpstreamStreamPolicy {
Auto,
ForceStream,
ForceNonStream,
}
pub fn parse_direct_request_body(
is_json_request: bool,
body_bytes: &[u8],
) -> Option<(serde_json::Value, Option<String>)> {
if is_json_request {
if body_bytes.is_empty() {
Some((serde_json::json!({}), None))
} else {
serde_json::from_slice::<serde_json::Value>(body_bytes)
.ok()
.map(|value| (value, None))
}
} else {
Some((
serde_json::json!({}),
(!body_bytes.is_empty())
.then(|| base64::engine::general_purpose::STANDARD.encode(body_bytes)),
))
}
}
pub fn force_upstream_streaming_for_provider(
provider_type: &str,
provider_api_format: &str,
) -> bool {
provider_type.trim().eq_ignore_ascii_case("codex")
&& aether_ai_formats::is_openai_responses_format(provider_api_format)
}
pub fn forbid_upstream_streaming_for_provider(
provider_type: &str,
provider_api_format: &str,
) -> bool {
aether_ai_formats::api_format_alias_matches(provider_api_format, "openai:search")
|| aether_ai_formats::is_openai_responses_compact_format(provider_api_format)
|| (provider_type.trim().eq_ignore_ascii_case("codex")
&& provider_api_format
.trim()
.eq_ignore_ascii_case("openai:image"))
}
pub(crate) fn parse_upstream_stream_policy(
value: Option<&serde_json::Value>,
) -> UpstreamStreamPolicy {
let Some(value) = value else {
return UpstreamStreamPolicy::Auto;
};
if let Some(value) = value.as_bool() {
return if value {
UpstreamStreamPolicy::ForceStream
} else {
UpstreamStreamPolicy::ForceNonStream
};
}
let serde_json::Value::String(value) = value else {
return UpstreamStreamPolicy::Auto;
};
let raw = value.trim().to_ascii_lowercase();
match raw.as_str() {
"" | "auto" | "follow" | "client" | "default" => UpstreamStreamPolicy::Auto,
"force_stream" | "stream" | "sse" | "true" | "1" | "yes" => {
UpstreamStreamPolicy::ForceStream
}
"force_non_stream" | "force_sync" | "non_stream" | "sync" | "false" | "0" | "no" => {
UpstreamStreamPolicy::ForceNonStream
}
_ => UpstreamStreamPolicy::Auto,
}
}
pub(crate) fn upstream_stream_policy_from_endpoint_config(
endpoint_config: Option<&serde_json::Value>,
) -> UpstreamStreamPolicy {
let Some(config) = endpoint_config.and_then(serde_json::Value::as_object) else {
return UpstreamStreamPolicy::Auto;
};
for key in [
"upstream_stream_policy",
"upstreamStreamPolicy",
"upstream_stream",
] {
if let Some(value) = config.get(key) {
return parse_upstream_stream_policy(Some(value));
}
}
UpstreamStreamPolicy::Auto
}
pub fn endpoint_config_forces_upstream_stream_policy(
endpoint_config: Option<&serde_json::Value>,
) -> bool {
matches!(
upstream_stream_policy_from_endpoint_config(endpoint_config),
UpstreamStreamPolicy::ForceStream | UpstreamStreamPolicy::ForceNonStream
)
}
/// Resolves the upstream provider transport mode.
///
/// `client_is_stream` means the request landed on a streaming surface or should
/// be treated as streaming; the original JSON body may not have had
/// `"stream": true`.
pub(crate) fn resolve_upstream_is_stream(
client_is_stream: bool,
hard_requires_streaming: bool,
policy: UpstreamStreamPolicy,
) -> bool {
// ForceStream is unconditional, while ForceNonStream yields to hard
// stream-only constraints such as Kiro or Codex OpenAI Responses.
match policy {
UpstreamStreamPolicy::ForceStream => true,
UpstreamStreamPolicy::ForceNonStream => hard_requires_streaming,
UpstreamStreamPolicy::Auto => hard_requires_streaming || client_is_stream,
}
}
pub fn enforce_request_body_stream_field(
body: &mut serde_json::Value,
provider_api_format: &str,
upstream_is_stream: bool,
require_body_stream_field: bool,
) {
let Some(body_object) = body.as_object_mut() else {
return;
};
if !api_format_uses_body_stream_field(provider_api_format) {
body_object.remove("stream");
return;
}
// Final-body fallback catches body rules, directive patches, and other
// provider-body mutations that introduce `stream`.
if upstream_is_stream || require_body_stream_field || body_object.contains_key("stream") {
body_object.insert(
"stream".to_string(),
serde_json::Value::Bool(upstream_is_stream),
);
} else {
body_object.remove("stream");
}
}
pub fn resolve_upstream_is_stream_from_endpoint_config(
endpoint_config: Option<&serde_json::Value>,
client_is_stream: bool,
hard_requires_streaming: bool,
) -> bool {
resolve_upstream_is_stream(
client_is_stream,
hard_requires_streaming,
upstream_stream_policy_from_endpoint_config(endpoint_config),
)
}
pub fn resolve_upstream_is_stream_for_provider(
endpoint_config: Option<&serde_json::Value>,
provider_type: &str,
provider_api_format: &str,
client_is_stream: bool,
hard_requires_streaming: bool,
) -> bool {
if forbid_upstream_streaming_for_provider(provider_type, provider_api_format) {
return false;
}
resolve_upstream_is_stream_from_endpoint_config(
endpoint_config,
client_is_stream,
hard_requires_streaming
|| force_upstream_streaming_for_provider(provider_type, provider_api_format),
)
}
#[cfg(test)]
mod tests {
use super::{
endpoint_config_forces_upstream_stream_policy, enforce_request_body_stream_field,
forbid_upstream_streaming_for_provider, force_upstream_streaming_for_provider,
parse_direct_request_body, parse_upstream_stream_policy, resolve_upstream_is_stream,
resolve_upstream_is_stream_for_provider, resolve_upstream_is_stream_from_endpoint_config,
upstream_stream_policy_from_endpoint_config, UpstreamStreamPolicy,
};
use serde_json::json;
#[test]
fn parses_empty_json_body_as_empty_object() {
assert_eq!(
parse_direct_request_body(true, b""),
Some((serde_json::json!({}), None))
);
}
#[test]
fn rejects_invalid_json_body() {
assert_eq!(parse_direct_request_body(true, b"{invalid"), None);
}
#[test]
fn encodes_non_json_body_as_base64() {
assert_eq!(
parse_direct_request_body(false, b"hello"),
Some((serde_json::json!({}), Some("aGVsbG8=".to_string())))
);
}
#[test]
fn forces_streaming_for_codex_openai_responses() {
assert!(force_upstream_streaming_for_provider(
"codex",
"openai:responses"
));
assert!(!force_upstream_streaming_for_provider(
"codex",
"openai:responses:compact"
));
}
#[test]
fn does_not_force_streaming_for_gemini_cli_generate_content() {
assert!(!force_upstream_streaming_for_provider(
"gemini_cli",
"gemini:generate_content"
));
assert!(!force_upstream_streaming_for_provider(
"google",
"gemini:generate_content"
));
}
#[test]
fn does_not_force_streaming_for_compact_or_other_provider_types() {
assert!(!force_upstream_streaming_for_provider(
"codex",
"openai:responses:compact"
));
assert!(!force_upstream_streaming_for_provider(
"openai",
"openai:responses"
));
}
#[test]
fn forbids_streaming_for_sync_only_openai_formats() {
assert!(forbid_upstream_streaming_for_provider(
"codex",
"openai:search"
));
assert!(forbid_upstream_streaming_for_provider(
"custom",
"/v1/alpha/search"
));
assert!(forbid_upstream_streaming_for_provider(
"codex",
"openai:responses:compact"
));
assert!(forbid_upstream_streaming_for_provider(
"openai",
"openai:responses:compact"
));
assert!(forbid_upstream_streaming_for_provider(
"custom",
"openai:responses:compact"
));
assert!(forbid_upstream_streaming_for_provider(
"codex",
"openai:image"
));
assert!(!forbid_upstream_streaming_for_provider(
"codex",
"openai:responses"
));
assert!(!forbid_upstream_streaming_for_provider(
"openai",
"openai:image"
));
}
#[test]
fn parses_python_compatible_upstream_stream_policy_values() {
assert_eq!(
parse_upstream_stream_policy(None),
UpstreamStreamPolicy::Auto
);
for value in [
json!(""),
json!("auto"),
json!("follow"),
json!("client"),
json!("default"),
json!("unknown"),
] {
assert_eq!(
parse_upstream_stream_policy(Some(&value)),
UpstreamStreamPolicy::Auto
);
}
for value in [
json!(true),
json!("force_stream"),
json!("stream"),
json!("sse"),
json!("true"),
json!("1"),
json!("yes"),
] {
assert_eq!(
parse_upstream_stream_policy(Some(&value)),
UpstreamStreamPolicy::ForceStream
);
}
for value in [
json!(false),
json!("force_non_stream"),
json!("force_sync"),
json!("non_stream"),
json!("sync"),
json!("false"),
json!("0"),
json!("no"),
] {
assert_eq!(
parse_upstream_stream_policy(Some(&value)),
UpstreamStreamPolicy::ForceNonStream
);
}
}
#[test]
fn parses_non_string_non_bool_policy_values_as_auto() {
for value in [json!(1), json!(0), json!(null), json!({}), json!([])] {
assert_eq!(
parse_upstream_stream_policy(Some(&value)),
UpstreamStreamPolicy::Auto
);
}
}
#[test]
fn enforces_request_body_stream_field_for_stream_and_streamless_formats() {
let mut openai_chat = json!({"stream": true});
enforce_request_body_stream_field(&mut openai_chat, "openai:chat", false, false);
assert_eq!(openai_chat.get("stream"), Some(&json!(false)));
let mut ordinary_sync = json!({"messages": []});
enforce_request_body_stream_field(&mut ordinary_sync, "openai:chat", false, false);
assert!(ordinary_sync.get("stream").is_none());
let mut forced_sync = json!({"messages": []});
enforce_request_body_stream_field(&mut forced_sync, "openai:chat", false, true);
assert_eq!(forced_sync.get("stream"), Some(&json!(false)));
let mut compact = json!({"stream": true});
enforce_request_body_stream_field(&mut compact, "openai:responses:compact", true, true);
assert!(compact.get("stream").is_none());
}
#[test]
fn reads_endpoint_policy_keys_in_python_compatible_order() {
assert_eq!(
upstream_stream_policy_from_endpoint_config(Some(&json!({
"upstream_stream_policy": "force_non_stream",
"upstreamStreamPolicy": "force_stream",
"upstream_stream": "force_stream"
}))),
UpstreamStreamPolicy::ForceNonStream
);
assert_eq!(
upstream_stream_policy_from_endpoint_config(Some(&json!({
"upstreamStreamPolicy": "force_stream"
}))),
UpstreamStreamPolicy::ForceStream
);
assert_eq!(
upstream_stream_policy_from_endpoint_config(Some(&json!({
"upstream_stream": false
}))),
UpstreamStreamPolicy::ForceNonStream
);
}
#[test]
fn detects_forced_endpoint_policy_values() {
assert!(endpoint_config_forces_upstream_stream_policy(Some(
&json!({"upstream_stream_policy": "force_stream"})
)));
assert!(endpoint_config_forces_upstream_stream_policy(Some(
&json!({"upstream_stream_policy": "force_non_stream"})
)));
assert!(!endpoint_config_forces_upstream_stream_policy(Some(
&json!({"upstream_stream_policy": "auto"})
)));
assert!(!endpoint_config_forces_upstream_stream_policy(None));
}
#[test]
fn resolves_upstream_stream_policy_against_client_mode_and_hard_constraints() {
assert!(resolve_upstream_is_stream(
false,
false,
UpstreamStreamPolicy::ForceStream
));
assert!(!resolve_upstream_is_stream(
true,
false,
UpstreamStreamPolicy::ForceNonStream
));
assert!(resolve_upstream_is_stream(
true,
true,
UpstreamStreamPolicy::ForceNonStream
));
assert!(!resolve_upstream_is_stream(
false,
false,
UpstreamStreamPolicy::Auto
));
assert!(resolve_upstream_is_stream(
true,
false,
UpstreamStreamPolicy::Auto
));
assert!(resolve_upstream_is_stream(
false,
true,
UpstreamStreamPolicy::Auto
));
}
#[test]
fn resolves_endpoint_policy_config_to_upstream_mode() {
assert!(resolve_upstream_is_stream_from_endpoint_config(
Some(&json!({"upstream_stream_policy": "force_stream"})),
false,
false,
));
assert!(!resolve_upstream_is_stream_from_endpoint_config(
Some(&json!({"upstream_stream_policy": "force_non_stream"})),
true,
false,
));
assert!(resolve_upstream_is_stream_from_endpoint_config(
Some(&json!({"upstream_stream_policy": "auto"})),
true,
false,
));
assert!(!resolve_upstream_is_stream_from_endpoint_config(
None, false, false,
));
}
#[test]
fn provider_policy_gives_non_stream_contracts_precedence() {
let force_stream = json!({"upstream_stream_policy": "force_stream"});
assert!(!resolve_upstream_is_stream_for_provider(
Some(&force_stream),
"codex",
"openai:search",
true,
true,
));
assert!(!resolve_upstream_is_stream_for_provider(
Some(&force_stream),
"codex",
"openai:responses:compact",
true,
true,
));
assert!(!resolve_upstream_is_stream_for_provider(
Some(&force_stream),
"codex",
"openai:image",
true,
true,
));
assert!(resolve_upstream_is_stream_for_provider(
Some(&json!({"upstream_stream_policy": "force_non_stream"})),
"codex",
"openai:responses",
false,
false,
));
}
}
@@ -0,0 +1,4 @@
pub use crate::formats::shared::standard_matrix::{
build_standard_request_body_from_canonical,
build_standard_request_body_from_canonical_with_model_directives,
};
@@ -0,0 +1,426 @@
use std::collections::BTreeMap;
use serde_json::Value;
use crate::contracts::core_success_background_report_kind;
use crate::formats::shared::request::UPSTREAM_IS_STREAM_KEY;
#[derive(Debug, Clone, PartialEq)]
pub struct LocalSyncReportParts {
pub trace_id: String,
pub report_kind: String,
pub report_context: Option<Value>,
pub status_code: u16,
pub headers: BTreeMap<String, String>,
pub body_json: Option<Value>,
pub client_body_json: Option<Value>,
pub body_base64: Option<String>,
}
pub fn build_generated_tool_call_id(index: usize) -> String {
format!("call_auto_{index}")
}
pub fn canonicalize_tool_arguments(value: Option<Value>) -> String {
match value {
Some(Value::String(text)) => text,
Some(other) => serde_json::to_string(&other).unwrap_or_else(|_| "null".to_string()),
None => "{}".to_string(),
}
}
pub fn remove_empty_pages_from_tool_arguments(tool_name: &str, arguments: &str) -> String {
if tool_name != "Read" {
return arguments.to_string();
}
let Ok(mut value) = serde_json::from_str::<Value>(arguments) else {
return arguments.to_string();
};
let Some(object) = value.as_object_mut() else {
return arguments.to_string();
};
if object.get("pages").and_then(Value::as_str) != Some("") {
return arguments.to_string();
}
object.remove("pages");
serde_json::to_string(&value).unwrap_or_else(|_| arguments.to_string())
}
pub fn remove_empty_pages_from_tool_input_value(tool_name: &str, input: &Value) -> Value {
if tool_name != "Read" || input.get("pages").and_then(Value::as_str) != Some("") {
return input.clone();
}
let Some(object) = input.as_object() else {
return input.clone();
};
let mut object = object.clone();
object.remove("pages");
Value::Object(object)
}
pub fn sanitize_claude_read_tool_inputs(value: &mut Value) -> bool {
let Some(content) = value.get_mut("content").and_then(Value::as_array_mut) else {
return false;
};
let mut changed = false;
for block in content {
let Some(block_object) = block.as_object_mut() else {
continue;
};
if block_object.get("type").and_then(Value::as_str) != Some("tool_use")
|| block_object.get("name").and_then(Value::as_str) != Some("Read")
{
continue;
}
let Some(input) = block_object.get("input") else {
continue;
};
let sanitized = remove_empty_pages_from_tool_input_value("Read", input);
if sanitized != *input {
block_object.insert("input".to_string(), sanitized);
changed = true;
}
}
changed
}
pub fn prepare_local_success_response_parts(
headers: &BTreeMap<String, String>,
body_json: &Value,
) -> serde_json::Result<(Vec<u8>, BTreeMap<String, String>)> {
prepare_local_success_response_parts_owned(headers.clone(), body_json)
}
pub fn prepare_local_success_response_parts_owned(
mut headers: BTreeMap<String, String>,
body_json: &Value,
) -> serde_json::Result<(Vec<u8>, BTreeMap<String, String>)> {
headers.remove("content-encoding");
headers.remove("content-length");
headers.insert("content-type".to_string(), "application/json".to_string());
let body_bytes = serde_json::to_vec(body_json)?;
headers.insert("content-length".to_string(), body_bytes.len().to_string());
Ok((body_bytes, headers))
}
fn should_capture_client_sync_success_body(payload: &LocalSyncReportParts) -> bool {
payload
.report_context
.as_ref()
.and_then(Value::as_object)
.and_then(|context| context.get(UPSTREAM_IS_STREAM_KEY))
.and_then(Value::as_bool)
.unwrap_or(false)
}
pub fn build_local_success_background_report(
payload: &LocalSyncReportParts,
body_json: Value,
headers: BTreeMap<String, String>,
) -> Option<LocalSyncReportParts> {
let report_kind = core_success_background_report_kind(payload.report_kind.as_str())?;
let upstream_is_stream = should_capture_client_sync_success_body(payload);
let client_body_json = upstream_is_stream.then(|| body_json.clone());
let provider_body_json = if upstream_is_stream {
payload.body_json.clone()
} else {
Some(body_json)
};
let provider_body_base64 = if upstream_is_stream {
payload.body_base64.clone()
} else {
None
};
Some(LocalSyncReportParts {
trace_id: payload.trace_id.clone(),
report_kind: report_kind.to_string(),
report_context: payload.report_context.clone(),
status_code: payload.status_code,
headers,
body_json: provider_body_json,
client_body_json,
body_base64: provider_body_base64,
})
}
pub fn build_local_success_conversion_background_report(
payload: &LocalSyncReportParts,
client_body_json: Value,
provider_body_json: Value,
) -> Option<LocalSyncReportParts> {
let report_kind = core_success_background_report_kind(payload.report_kind.as_str())?;
Some(LocalSyncReportParts {
trace_id: payload.trace_id.clone(),
report_kind: report_kind.to_string(),
report_context: payload.report_context.clone(),
status_code: payload.status_code,
headers: payload.headers.clone(),
body_json: Some(provider_body_json),
client_body_json: Some(client_body_json),
body_base64: None,
})
}
#[cfg(test)]
mod tests {
use base64::Engine as _;
use serde_json::Value;
use super::{
build_generated_tool_call_id, build_local_success_background_report,
build_local_success_conversion_background_report, canonicalize_tool_arguments,
prepare_local_success_response_parts, prepare_local_success_response_parts_owned,
remove_empty_pages_from_tool_arguments, sanitize_claude_read_tool_inputs,
LocalSyncReportParts,
};
use std::collections::BTreeMap;
#[test]
fn generated_tool_call_ids_are_stable() {
assert_eq!(build_generated_tool_call_id(3), "call_auto_3");
}
#[test]
fn canonicalizes_tool_arguments() {
assert_eq!(
canonicalize_tool_arguments(Some(serde_json::json!({"x": 1}))),
"{\"x\":1}"
);
assert_eq!(canonicalize_tool_arguments(None), "{}");
}
#[test]
fn removes_empty_pages_from_tool_arguments() {
assert_eq!(
remove_empty_pages_from_tool_arguments(
"Read",
r#"{"file_path":"/tmp/a.txt","offset":1,"limit":20,"pages":""}"#
),
r#"{"file_path":"/tmp/a.txt","offset":1,"limit":20}"#
);
assert_eq!(
remove_empty_pages_from_tool_arguments("Search", r#"{"query":"","pages":""}"#),
r#"{"query":"","pages":""}"#
);
assert_eq!(
remove_empty_pages_from_tool_arguments("Read", r#"{"pages":"1-2"}"#),
r#"{"pages":"1-2"}"#
);
assert_eq!(
remove_empty_pages_from_tool_arguments("Read", r#"{"pages":"#),
r#"{"pages":"#
);
}
#[test]
fn sanitizes_claude_read_tool_inputs_only() {
let mut value = serde_json::json!({
"content": [
{
"type": "tool_use",
"name": "Read",
"input": {
"file_path": "/tmp/a.txt",
"limit": 20,
"pages": ""
}
},
{
"type": "tool_use",
"name": "Search",
"input": {
"query": "",
"pages": ""
}
},
{
"type": "tool_use",
"name": "Read",
"input": {
"pages": "1-2"
}
}
]
});
assert!(sanitize_claude_read_tool_inputs(&mut value));
assert_eq!(
value["content"][0]["input"],
serde_json::json!({
"file_path": "/tmp/a.txt",
"limit": 20,
})
);
assert_eq!(
value["content"][1]["input"],
serde_json::json!({
"query": "",
"pages": "",
})
);
assert_eq!(
value["content"][2]["input"],
serde_json::json!({"pages": "1-2"})
);
}
#[test]
fn prepare_local_success_response_parts_normalizes_headers() {
let headers = BTreeMap::from([
("content-encoding".to_string(), "gzip".to_string()),
("content-length".to_string(), "999".to_string()),
("x-test".to_string(), "1".to_string()),
]);
let (body_bytes, normalized_headers) =
prepare_local_success_response_parts(&headers, &serde_json::json!({"ok": true}))
.expect("response parts should serialize");
assert_eq!(
serde_json::from_slice::<Value>(&body_bytes).expect("json body"),
serde_json::json!({"ok": true})
);
assert_eq!(
normalized_headers.get("content-type").map(String::as_str),
Some("application/json")
);
assert!(!normalized_headers.contains_key("content-encoding"));
let expected_length = body_bytes.len().to_string();
assert_eq!(
normalized_headers.get("content-length").map(String::as_str),
Some(expected_length.as_str())
);
assert_eq!(
normalized_headers.get("x-test").map(String::as_str),
Some("1")
);
}
#[test]
fn prepare_local_success_response_parts_owned_normalizes_headers() {
let headers = BTreeMap::from([
("content-encoding".to_string(), "gzip".to_string()),
("content-length".to_string(), "999".to_string()),
("x-test".to_string(), "1".to_string()),
]);
let (body_bytes, normalized_headers) =
prepare_local_success_response_parts_owned(headers, &serde_json::json!({"ok": true}))
.expect("response parts should serialize");
assert_eq!(
serde_json::from_slice::<Value>(&body_bytes).expect("json body"),
serde_json::json!({"ok": true})
);
assert_eq!(
normalized_headers.get("content-type").map(String::as_str),
Some("application/json")
);
assert!(!normalized_headers.contains_key("content-encoding"));
let expected_length = body_bytes.len().to_string();
assert_eq!(
normalized_headers.get("content-length").map(String::as_str),
Some(expected_length.as_str())
);
assert_eq!(
normalized_headers.get("x-test").map(String::as_str),
Some("1")
);
}
#[test]
fn build_local_success_background_report_maps_finalize_kind() {
let payload = LocalSyncReportParts {
trace_id: "trace-1".to_string(),
report_kind: "openai_chat_sync_finalize".to_string(),
report_context: Some(serde_json::json!({"request_id": "req-1"})),
status_code: 200,
headers: BTreeMap::from([("x-test".to_string(), "1".to_string())]),
body_json: None,
client_body_json: None,
body_base64: None,
};
let report = build_local_success_background_report(
&payload,
serde_json::json!({"id": "resp-1"}),
payload.headers.clone(),
)
.expect("success report should be built");
assert_eq!(report.report_kind, "openai_chat_sync_success");
assert_eq!(report.body_json, Some(serde_json::json!({"id": "resp-1"})));
assert_eq!(report.client_body_json, None);
}
#[test]
fn build_local_success_background_report_preserves_provider_stream_for_upstream_stream_sync() {
let payload = LocalSyncReportParts {
trace_id: "trace-1b".to_string(),
report_kind: "openai_chat_sync_finalize".to_string(),
report_context: Some(serde_json::json!({
"request_id": "req-1b",
"upstream_is_stream": true
})),
status_code: 200,
headers: BTreeMap::from([("content-type".to_string(), "text/event-stream".to_string())]),
body_json: None,
client_body_json: None,
body_base64: Some(base64::engine::general_purpose::STANDARD.encode(
concat!(
"event: response.created\n",
"data: {\"type\":\"response.created\",\"response\":{\"id\":\"resp-1b\",\"object\":\"response\",\"status\":\"in_progress\",\"output\":[]}}\n\n",
"event: response.output_text.delta\n",
"data: {\"type\":\"response.output_text.delta\",\"delta\":\"hello\"}\n\n",
"event: response.completed\n",
"data: {\"type\":\"response.completed\",\"response\":{\"id\":\"resp-1b\",\"object\":\"response\",\"status\":\"completed\",\"output\":[],\"usage\":{\"input_tokens\":1,\"output_tokens\":1,\"total_tokens\":2}}}\n\n",
)
)),
};
let report = build_local_success_background_report(
&payload,
serde_json::json!({"id": "resp-1b"}),
payload.headers.clone(),
)
.expect("success report should be built");
assert_eq!(report.body_json, None);
assert_eq!(
report.client_body_json,
Some(serde_json::json!({"id": "resp-1b"}))
);
assert_eq!(report.body_base64, payload.body_base64);
}
#[test]
fn build_local_success_conversion_background_report_maps_provider_body() {
let payload = LocalSyncReportParts {
trace_id: "trace-2".to_string(),
report_kind: "openai_chat_sync_finalize".to_string(),
report_context: Some(serde_json::json!({"request_id": "req-2"})),
status_code: 200,
headers: BTreeMap::from([("content-type".to_string(), "application/json".to_string())]),
body_json: None,
client_body_json: None,
body_base64: None,
};
let report = build_local_success_conversion_background_report(
&payload,
serde_json::json!({"client": true}),
serde_json::json!({"provider": true}),
)
.expect("conversion success report should be built");
assert_eq!(report.report_kind, "openai_chat_sync_success");
assert_eq!(
report.body_json,
Some(serde_json::json!({"provider": true}))
);
assert_eq!(
report.client_body_json,
Some(serde_json::json!({"client": true}))
);
}
}
File diff suppressed because it is too large Load Diff
@@ -0,0 +1,39 @@
use serde_json::Value;
use crate::formats::shared::AiSurfaceFinalizeError;
pub fn map_claude_stop_reason(stop_reason: Option<&str>, has_tool_calls: bool) -> Option<String> {
let mapped = match stop_reason {
Some("end_turn") | Some("stop_sequence") => Some("stop".to_string()),
Some("max_tokens") => Some("length".to_string()),
Some("tool_use") => Some("tool_calls".to_string()),
Some("pause_turn") => Some("stop".to_string()),
Some(other) if !other.trim().is_empty() => Some(other.to_string()),
_ => None,
};
if has_tool_calls && mapped.as_deref().is_none_or(|value| value == "stop") {
Some("tool_calls".to_string())
} else {
mapped
}
}
pub fn encode_done_sse() -> Vec<u8> {
b"data: [DONE]\n\n".to_vec()
}
pub fn encode_json_sse(
event: Option<&str>,
value: &Value,
) -> Result<Vec<u8>, AiSurfaceFinalizeError> {
let mut out = Vec::new();
if let Some(event) = event.filter(|value| !value.trim().is_empty()) {
out.extend_from_slice(b"event: ");
out.extend_from_slice(event.as_bytes());
out.push(b'\n');
}
out.extend_from_slice(b"data: ");
out.extend(serde_json::to_vec(value).map_err(AiSurfaceFinalizeError::from)?);
out.extend_from_slice(b"\n\n");
Ok(out)
}
File diff suppressed because it is too large Load Diff
@@ -0,0 +1,904 @@
use std::borrow::Cow;
use aether_ai_formats::formats::conversion::request::{
convert_openai_chat_request_to_claude_request, convert_openai_chat_request_to_gemini_request,
convert_openai_chat_request_to_openai_responses_request,
normalize_claude_request_to_openai_chat_request,
normalize_gemini_request_to_openai_chat_request,
normalize_openai_responses_request_to_openai_chat_request,
};
use aether_ai_formats::{request_conversion_kind, FormatContext, RequestConversionKind};
use serde_json::{json, Value};
use crate::formats::shared::model_directives::apply_model_directive_overrides_from_request;
fn is_responses_shaped_body_on_chat_endpoint(body_json: &Value) -> bool {
body_json
.as_object()
.is_some_and(|object| !object.contains_key("messages") && object.contains_key("input"))
}
pub fn is_claude_messages_shaped_body_on_openai_chat_endpoint(body_json: &Value) -> bool {
let Some(request) = body_json.as_object() else {
return false;
};
if !request.contains_key("messages") {
return false;
}
request
.get("tools")
.and_then(Value::as_array)
.is_some_and(|tools| tools.iter().any(is_claude_native_tool_definition))
|| request
.get("messages")
.and_then(Value::as_array)
.is_some_and(|messages| messages.iter().any(message_has_claude_tool_block))
}
fn is_claude_native_tool_definition(tool: &Value) -> bool {
tool.as_object().is_some_and(|tool_object| {
tool_object.contains_key("input_schema") && !tool_object.contains_key("function")
})
}
fn message_has_claude_tool_block(message: &Value) -> bool {
message
.as_object()
.and_then(|object| object.get("content"))
.and_then(Value::as_array)
.is_some_and(|parts| parts.iter().any(is_claude_tool_content_block))
}
fn is_claude_tool_content_block(part: &Value) -> bool {
part.as_object()
.and_then(|object| object.get("type"))
.and_then(Value::as_str)
.is_some_and(|block_type| matches!(block_type, "tool_use" | "tool_result"))
}
fn chat_compatible_body_for_openai_chat_endpoint(body_json: &Value) -> Option<Cow<'_, Value>> {
if is_responses_shaped_body_on_chat_endpoint(body_json) {
return normalize_openai_responses_request_to_openai_chat_request(body_json)
.map(Cow::Owned);
}
Some(Cow::Borrowed(body_json))
}
fn chat_compatible_body_for_standard_source<'a>(
body_json: &'a Value,
client_api_format: &str,
) -> Option<Cow<'a, Value>> {
match aether_ai_formats::normalize_api_format_alias(client_api_format).as_str() {
"openai:chat" => chat_compatible_body_for_openai_chat_endpoint(body_json),
"openai:responses" | "openai:responses:compact" => {
normalize_openai_responses_request_to_openai_chat_request(body_json).map(Cow::Owned)
}
"claude:messages" => {
normalize_claude_request_to_openai_chat_request(body_json).map(Cow::Owned)
}
"gemini:generate_content" => {
normalize_gemini_request_to_openai_chat_request(body_json, "").map(Cow::Owned)
}
_ => None,
}
}
pub fn build_local_openai_chat_request_body(
body_json: &Value,
mapped_model: &str,
upstream_is_stream: bool,
) -> Option<Value> {
build_local_openai_chat_request_body_with_model_directives(
body_json,
mapped_model,
upstream_is_stream,
false,
)
}
pub fn build_local_openai_chat_request_body_with_model_directives(
body_json: &Value,
mapped_model: &str,
upstream_is_stream: bool,
enable_model_directives: bool,
) -> Option<Value> {
let chat_body = chat_compatible_body_for_openai_chat_endpoint(body_json)?;
let request_body_object = chat_body.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,
}),
);
}
}
}
let mut provider_request_body = with_model_directive_overrides(
Value::Object(provider_request_body),
"openai:chat",
mapped_model,
body_json,
None,
enable_model_directives,
);
let require_body_stream_field = body_json
.as_object()
.is_some_and(|object| object.contains_key("stream"));
crate::formats::shared::request::enforce_request_body_stream_field(
&mut provider_request_body,
"openai:chat",
upstream_is_stream,
require_body_stream_field,
);
Some(provider_request_body)
}
pub fn build_cross_format_openai_chat_request_body(
body_json: &Value,
mapped_model: &str,
provider_api_format: &str,
upstream_is_stream: bool,
) -> Option<Value> {
build_cross_format_openai_chat_request_body_with_model_directives(
body_json,
mapped_model,
provider_api_format,
upstream_is_stream,
false,
)
}
pub fn build_cross_format_openai_chat_request_body_with_model_directives(
body_json: &Value,
mapped_model: &str,
provider_api_format: &str,
upstream_is_stream: bool,
enable_model_directives: bool,
) -> Option<Value> {
let conversion_kind = request_conversion_kind("openai:chat", provider_api_format)?;
let provider_request_body = match conversion_kind {
RequestConversionKind::ToClaudeStandard => {
if is_claude_messages_shaped_body_on_openai_chat_endpoint(body_json) {
convert_claude_compatible_chat_endpoint_request(
body_json,
mapped_model,
provider_api_format,
upstream_is_stream,
)?
} else {
let chat_body = chat_compatible_body_for_openai_chat_endpoint(body_json)?;
convert_openai_chat_request_to_claude_request(
chat_body.as_ref(),
mapped_model,
upstream_is_stream,
)?
}
}
RequestConversionKind::ToGeminiStandard => {
let chat_body = chat_compatible_body_for_openai_chat_endpoint(body_json)?;
convert_openai_chat_request_to_gemini_request(
chat_body.as_ref(),
mapped_model,
upstream_is_stream,
)?
}
RequestConversionKind::ToOpenAiResponses => {
if is_responses_shaped_body_on_chat_endpoint(body_json) {
build_local_openai_responses_request_body_with_model_directives(
body_json,
mapped_model,
upstream_is_stream,
enable_model_directives,
)?
} else {
convert_openai_chat_request_to_openai_responses_request(
body_json,
mapped_model,
upstream_is_stream,
false,
)?
}
}
_ => return None,
};
let mut provider_request_body = with_model_directive_overrides(
provider_request_body,
provider_api_format,
mapped_model,
body_json,
None,
enable_model_directives,
);
let require_body_stream_field = body_json
.as_object()
.is_some_and(|object| object.contains_key("stream"));
crate::formats::shared::request::enforce_request_body_stream_field(
&mut provider_request_body,
provider_api_format,
upstream_is_stream,
require_body_stream_field,
);
Some(provider_request_body)
}
fn convert_claude_compatible_chat_endpoint_request(
body_json: &Value,
mapped_model: &str,
provider_api_format: &str,
upstream_is_stream: bool,
) -> Option<Value> {
aether_ai_formats::convert_request(
"claude:messages",
provider_api_format,
body_json,
&FormatContext::default()
.with_mapped_model(mapped_model)
.with_upstream_stream(upstream_is_stream),
)
.ok()
}
pub fn build_local_openai_responses_request_body(
body_json: &Value,
mapped_model: &str,
require_streaming: bool,
) -> Option<Value> {
build_local_openai_responses_request_body_with_model_directives(
body_json,
mapped_model,
require_streaming,
false,
)
}
pub fn build_local_openai_responses_request_body_with_model_directives(
body_json: &Value,
mapped_model: &str,
require_streaming: bool,
enable_model_directives: 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));
}
let mut provider_request_body = with_model_directive_overrides(
Value::Object(provider_request_body),
"openai:responses",
mapped_model,
body_json,
None,
enable_model_directives,
);
let require_body_stream_field = body_json
.as_object()
.is_some_and(|object| object.contains_key("stream"));
crate::formats::shared::request::enforce_request_body_stream_field(
&mut provider_request_body,
"openai:responses",
require_streaming,
require_body_stream_field,
);
Some(provider_request_body)
}
pub fn build_cross_format_openai_responses_request_body(
body_json: &Value,
mapped_model: &str,
client_api_format: &str,
provider_api_format: &str,
upstream_is_stream: bool,
) -> Option<Value> {
build_cross_format_openai_responses_request_body_with_model_directives(
body_json,
mapped_model,
client_api_format,
provider_api_format,
upstream_is_stream,
false,
)
}
pub fn build_cross_format_openai_responses_request_body_with_model_directives(
body_json: &Value,
mapped_model: &str,
client_api_format: &str,
provider_api_format: &str,
upstream_is_stream: bool,
enable_model_directives: bool,
) -> Option<Value> {
let chat_like_request = chat_compatible_body_for_standard_source(body_json, client_api_format)?;
let conversion_kind = request_conversion_kind(client_api_format, provider_api_format)?;
let provider_request_body = match conversion_kind {
RequestConversionKind::ToOpenAIChat => {
build_local_openai_chat_request_body_with_model_directives(
chat_like_request.as_ref(),
mapped_model,
upstream_is_stream,
enable_model_directives,
)?
}
RequestConversionKind::ToOpenAiResponses => {
convert_openai_chat_request_to_openai_responses_request(
chat_like_request.as_ref(),
mapped_model,
upstream_is_stream,
false,
)?
}
RequestConversionKind::ToClaudeStandard => convert_openai_chat_request_to_claude_request(
chat_like_request.as_ref(),
mapped_model,
upstream_is_stream,
)?,
RequestConversionKind::ToGeminiStandard => convert_openai_chat_request_to_gemini_request(
chat_like_request.as_ref(),
mapped_model,
upstream_is_stream,
)?,
};
let mut provider_request_body = with_model_directive_overrides(
provider_request_body,
provider_api_format,
mapped_model,
body_json,
None,
enable_model_directives,
);
let require_body_stream_field = body_json
.as_object()
.is_some_and(|object| object.contains_key("stream"));
crate::formats::shared::request::enforce_request_body_stream_field(
&mut provider_request_body,
provider_api_format,
upstream_is_stream,
require_body_stream_field,
);
Some(provider_request_body)
}
fn with_model_directive_overrides(
mut provider_request_body: Value,
provider_api_format: &str,
provider_model: &str,
request_body: &Value,
request_path: Option<&str>,
enable_model_directives: bool,
) -> Value {
if enable_model_directives {
apply_model_directive_overrides_from_request(
&mut provider_request_body,
provider_api_format,
provider_model,
request_body,
request_path,
);
}
provider_request_body
}
#[cfg(test)]
mod tests {
use super::build_local_openai_responses_request_body;
use super::{
build_cross_format_openai_chat_request_body_with_model_directives,
build_cross_format_openai_responses_request_body, build_local_openai_chat_request_body,
build_local_openai_chat_request_body_with_model_directives,
build_local_openai_responses_request_body_with_model_directives,
};
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 local_openai_chat_request_body_accepts_responses_shape_from_chat_endpoint() {
let body_json = json!({
"model": "gpt-5",
"stream": true,
"input": [{"role": "user", "content": "hello"}],
"tools": [{
"type": "function",
"name": "Shell",
"parameters": {"type": "object"},
"strict": false
}],
"reasoning": {"effort": "high"}
});
let provider_request_body =
build_local_openai_chat_request_body(&body_json, "gpt-5-upstream", true)
.expect("responses-shaped chat body should build as chat");
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");
assert_eq!(
provider_request_body["tools"][0]["function"]["name"],
"Shell"
);
assert_eq!(provider_request_body["reasoning_effort"], "high");
assert_eq!(provider_request_body["stream"], true);
assert_eq!(
provider_request_body["stream_options"]["include_usage"],
true
);
}
#[test]
fn cross_format_openai_chat_request_body_preserves_responses_shape_for_responses_target() {
let body_json = json!({
"model": "gpt-5",
"stream": true,
"input": [{"role": "user", "content": "hello"}],
"include": ["reasoning.encrypted_content"],
"stream_options": {"include_usage": true},
"tools": [{
"type": "function",
"name": "Shell",
"parameters": {"type": "object"},
"strict": false
}, {
"type": "function",
"parameters": {"type": "object"}
}]
});
let provider_request_body =
build_cross_format_openai_chat_request_body_with_model_directives(
&body_json,
"gpt-5-upstream",
"openai:responses",
false,
false,
)
.expect("responses-shaped chat body should build as responses");
assert_eq!(provider_request_body["model"], "gpt-5-upstream");
assert_eq!(provider_request_body["input"][0]["role"], "user");
assert_eq!(provider_request_body["input"][0]["content"], "hello");
assert_eq!(provider_request_body["tools"][0]["name"], "Shell");
assert_eq!(provider_request_body["tools"][0]["strict"], false);
assert_eq!(provider_request_body["tools"][1]["type"], "function");
assert_eq!(
provider_request_body["include"][0],
"reasoning.encrypted_content"
);
assert_eq!(
provider_request_body["stream_options"]["include_usage"],
true
);
assert_eq!(provider_request_body["stream"], false);
assert!(provider_request_body.get("messages").is_none());
}
#[test]
fn cross_format_openai_responses_body_preserves_chat_messages_for_chat_source() {
let body_json = json!({
"model": "gpt-5.5",
"messages": [
{"role": "system", "content": "Return a JSON object."},
{"role": "user", "content": "Explain why this JSON patch failed."}
],
"response_format": {"type": "json_object"}
});
let provider_request_body = build_cross_format_openai_responses_request_body(
&body_json,
"gpt-5.5-upstream",
"openai:chat",
"openai:responses",
false,
)
.expect("openai chat to openai responses body should build");
assert_eq!(provider_request_body["model"], "gpt-5.5-upstream");
assert_eq!(
provider_request_body["text"]["format"]["type"],
"json_object"
);
assert_eq!(provider_request_body["input"][0]["role"], "user");
assert_eq!(
provider_request_body["input"][0]["content"][0]["text"],
"Explain why this JSON patch failed."
);
assert!(provider_request_body.get("messages").is_none());
}
#[test]
fn openai_chat_request_body_prefers_messages_when_messages_and_input_are_both_present() {
let body_json = json!({
"model": "gpt-5",
"messages": [{"role": "user", "content": "from messages"}],
"input": [{"role": "user", "content": "from input"}]
});
let provider_request_body =
build_cross_format_openai_chat_request_body_with_model_directives(
&body_json,
"gpt-5-upstream",
"openai:responses",
false,
false,
)
.expect("normal chat body should still use messages");
assert_eq!(
provider_request_body["input"][0]["content"][0]["text"],
"from messages"
);
}
#[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 local_openai_chat_request_body_overrides_client_stream_for_non_stream_upstream() {
let body_json = json!({
"model": "gpt-5",
"messages": [{
"role": "user",
"content": "hello"
}],
"stream": true
});
let provider_request_body =
build_local_openai_chat_request_body(&body_json, "gpt-5-upstream", false)
.expect("openai chat body should build");
assert_eq!(provider_request_body["model"], "gpt-5-upstream");
assert_eq!(provider_request_body["stream"], false);
}
#[test]
fn local_openai_responses_request_body_overrides_client_stream_for_non_stream_upstream() {
let body_json = json!({
"model": "gpt-5",
"input": "hello",
"stream": true
});
let provider_request_body =
build_local_openai_responses_request_body(&body_json, "gpt-5-upstream", false)
.expect("openai responses body should build");
assert_eq!(provider_request_body["model"], "gpt-5-upstream");
assert_eq!(provider_request_body["stream"], false);
}
#[test]
fn cross_format_openai_chat_request_body_overrides_client_stream_for_non_stream_upstream() {
let body_json = json!({
"model": "gpt-5",
"messages": [{"role": "user", "content": "hello"}],
"stream": true
});
let claude = build_cross_format_openai_chat_request_body_with_model_directives(
&body_json,
"claude-sonnet-4-5",
"claude:messages",
false,
false,
)
.expect("claude body should build");
assert_eq!(claude["stream"], false);
let responses = build_cross_format_openai_chat_request_body_with_model_directives(
&body_json,
"gpt-5-upstream",
"openai:responses",
false,
false,
)
.expect("responses body should build");
assert_eq!(responses["stream"], false);
}
#[test]
fn cross_format_openai_chat_request_body_does_not_add_stream_false_for_plain_sync_body() {
let body_json = json!({
"model": "gpt-5",
"messages": [{"role": "user", "content": "hello"}]
});
let claude = build_cross_format_openai_chat_request_body_with_model_directives(
&body_json,
"claude-sonnet-4-5",
"claude:messages",
false,
false,
)
.expect("claude body should build");
assert!(claude.get("stream").is_none());
}
#[test]
fn cross_format_openai_responses_body_overrides_client_stream_for_non_stream_upstream() {
let body_json = json!({
"model": "gpt-5",
"input": "hello",
"stream": true
});
let provider_request_body = build_cross_format_openai_responses_request_body(
&body_json,
"claude-sonnet-4-5",
"openai:responses",
"claude:messages",
false,
)
.expect("claude body should build");
assert_eq!(provider_request_body["stream"], false);
}
#[test]
fn local_openai_chat_request_body_applies_reasoning_effort_suffix() {
let body_json = json!({
"model": "gpt-5.4-xhigh",
"messages": [{"role": "user", "content": "hello"}],
"reasoning_effort": "low"
});
let provider_request_body = build_local_openai_chat_request_body_with_model_directives(
&body_json,
"gpt-5-upstream",
false,
true,
)
.expect("openai chat body should build");
assert_eq!(provider_request_body["model"], "gpt-5-upstream");
assert_eq!(provider_request_body["reasoning_effort"], "xhigh");
}
#[test]
fn local_openai_chat_request_body_leaves_model_directive_disabled_by_default() {
let body_json = json!({
"model": "gpt-5.4-xhigh",
"messages": [{"role": "user", "content": "hello"}],
"reasoning_effort": "low"
});
let provider_request_body =
build_local_openai_chat_request_body(&body_json, "gpt-5-upstream", false)
.expect("openai chat body should build");
assert_eq!(provider_request_body["model"], "gpt-5-upstream");
assert_eq!(provider_request_body["reasoning_effort"], "low");
}
#[test]
fn local_openai_responses_request_body_applies_reasoning_effort_suffix() {
let body_json = json!({
"model": "gpt-5.6-sol-max",
"input": "hello",
"reasoning": {"effort": "low", "summary": "auto"}
});
let provider_request_body =
build_local_openai_responses_request_body_with_model_directives(
&body_json,
"gpt-5-upstream",
false,
true,
)
.expect("openai responses body should build");
assert_eq!(provider_request_body["model"], "gpt-5-upstream");
assert_eq!(provider_request_body["reasoning"]["summary"], "auto");
assert_eq!(provider_request_body["reasoning"]["effort"], "max");
}
#[test]
fn cross_format_request_body_applies_reasoning_effort_suffix() {
let body_json = json!({
"model": "gpt-5.4-high",
"messages": [{"role": "user", "content": "hello"}],
"reasoning_effort": "low"
});
let provider_request_body =
build_cross_format_openai_chat_request_body_with_model_directives(
&body_json,
"claude-sonnet-4-5",
"claude:messages",
false,
true,
)
.expect("claude body should build");
assert_eq!(provider_request_body["model"], "claude-sonnet-4-5");
assert_eq!(provider_request_body["output_config"]["effort"], "high");
assert_eq!(provider_request_body["thinking"]["budget_tokens"], 4096);
}
#[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");
}
#[test]
fn cross_format_openai_chat_request_body_accepts_claude_native_messages_for_claude_target() {
let body_json = json!({
"model": "deepseek-v4-flash",
"messages": [
{"role": "user", "content": "lookup"},
{
"role": "assistant",
"content": [
{"type": "text", "text": "checking"},
{
"type": "tool_use",
"id": "call_1",
"name": "lookup",
"input": {"q": "db"}
}
]
},
{
"role": "user",
"content": [{
"type": "tool_result",
"tool_use_id": "call_1",
"content": {"rows": 1}
}]
}
],
"tools": [{
"name": "lookup",
"description": "Lookup data",
"input_schema": {"type": "object", "properties": {"q": {"type": "string"}}}
}],
"tool_choice": {"type": "auto"},
"max_tokens": 128,
"stream": true
});
let provider_request_body =
build_cross_format_openai_chat_request_body_with_model_directives(
&body_json,
"claude-sonnet-4-5",
"claude:messages",
true,
false,
)
.expect("claude-native chat endpoint body should build as claude messages");
assert_eq!(provider_request_body["model"], "claude-sonnet-4-5");
assert_eq!(provider_request_body["tools"][0]["name"], "lookup");
assert_eq!(
provider_request_body["tools"][0]["input_schema"]["properties"]["q"]["type"],
"string"
);
assert_eq!(
provider_request_body["messages"][1]["content"][1]["type"],
"tool_use"
);
assert_eq!(
provider_request_body["messages"][2]["content"][0]["type"],
"tool_result"
);
assert_eq!(
serde_json::from_str::<Value>(
provider_request_body["messages"][2]["content"][0]["content"]
.as_str()
.expect("object tool result content should be serialized for Claude")
)
.expect("serialized tool result content should remain JSON"),
json!({"rows": 1})
);
assert_eq!(provider_request_body["tool_choice"]["type"], "auto");
assert_eq!(provider_request_body["stream"], true);
}
}
@@ -0,0 +1,836 @@
use serde_json::{json, Map, Value};
use crate::formats::shared::model_directives::model_directive_display_model_from_report_context;
pub use aether_ai_formats::protocol::stream::{
CanonicalContentPart, CanonicalStreamEvent, CanonicalStreamFrame, CanonicalUsage,
};
pub fn decode_json_data_line(line: &[u8]) -> Option<Value> {
let text = std::str::from_utf8(line).ok()?;
let trimmed = text.trim_matches('\r').trim();
if trimmed.is_empty() || trimmed.starts_with(':') || trimmed.starts_with("event:") {
return None;
}
let data_line = trimmed.strip_prefix("data:")?.trim();
if data_line.is_empty() || data_line == "[DONE]" {
return None;
}
serde_json::from_str(data_line).ok()
}
pub fn unsupported_stream_event_message(payload: &Value) -> String {
const BASE_MESSAGE: &str = "Unsupported provider stream event cannot be converted losslessly";
match unsupported_stream_event_diagnostic(payload) {
Some(diagnostic) if !diagnostic.is_empty() => format!("{BASE_MESSAGE}: {diagnostic}"),
_ => BASE_MESSAGE.to_string(),
}
}
fn unsupported_stream_event_diagnostic(payload: &Value) -> Option<String> {
let mut details = Vec::new();
if let Some((path, value)) = unsupported_stream_event_primary_field(payload) {
details.push(format!("field {path} = {value}"));
} else if let Some(path) = unsupported_stream_event_single_field(payload) {
details.push(format!("field {path} is unsupported"));
}
if let Some(fields) = unsupported_stream_event_field_list(payload) {
details.push(format!("fields: {fields}"));
}
if details.is_empty() {
None
} else {
Some(details.join("; "))
}
}
fn unsupported_stream_event_primary_field(payload: &Value) -> Option<(&'static str, String)> {
const STRING_FIELD_PATHS: &[(&str, &str)] = &[
("$.item.type", "/item/type"),
("$.content_block.type", "/content_block/type"),
("$.delta.type", "/delta/type"),
("$.part.type", "/part/type"),
("$.payload.type", "/payload/type"),
("$.type", "/type"),
("$.event", "/event"),
];
STRING_FIELD_PATHS
.iter()
.find_map(|(display_path, pointer)| {
payload
.pointer(pointer)
.and_then(Value::as_str)
.filter(|value| !value.trim().is_empty())
.map(|value| (*display_path, json!(value.trim()).to_string()))
})
}
fn unsupported_stream_event_single_field(payload: &Value) -> Option<String> {
let object = payload.as_object()?;
if object.len() != 1 {
return None;
}
object.keys().next().map(|key| json_path_key(key))
}
fn unsupported_stream_event_field_list(payload: &Value) -> Option<String> {
let object = payload.as_object()?;
if object.is_empty() {
return None;
}
let fields = object
.keys()
.take(8)
.map(|key| key.as_str())
.collect::<Vec<_>>()
.join(", ");
if object.len() > 8 {
Some(format!("{fields}, ..."))
} else {
Some(fields)
}
}
fn json_path_key(key: &str) -> String {
if !key.is_empty()
&& key.chars().enumerate().all(|(index, ch)| {
ch == '_' || ch.is_ascii_alphabetic() || (index > 0 && ch.is_ascii_digit())
})
{
format!("$.{key}")
} else {
format!("$[{}]", json!(key))
}
}
pub fn resolve_identity(
response_id: Option<&str>,
model: Option<&str>,
report_context: &Value,
default_id: &str,
) -> (String, String) {
let id = response_id
.filter(|value| !value.is_empty())
.unwrap_or(default_id)
.to_string();
if let Some(display_model) = model_directive_display_model_from_report_context(report_context) {
return (id, display_model);
}
let model = model
.filter(|value| !value.is_empty())
.or_else(|| report_context.get("mapped_model").and_then(Value::as_str))
.or_else(|| report_context.get("model").and_then(Value::as_str))
.unwrap_or("unknown")
.to_string();
(id, model)
}
pub fn canonical_usage_from_openai_usage(value: Option<&Value>) -> Option<CanonicalUsage> {
let usage = value?.as_object()?;
let mut input_tokens = usage
.get("input_tokens")
.or_else(|| usage.get("prompt_tokens"))
.and_then(Value::as_u64)
.unwrap_or(0);
let output_tokens = usage
.get("output_tokens")
.or_else(|| usage.get("completion_tokens"))
.and_then(Value::as_u64)
.unwrap_or(0);
let cache_creation_tokens = usage
.get("cache_creation_input_tokens")
.and_then(Value::as_u64)
.or_else(|| {
usage
.get("input_tokens_details")
.or_else(|| usage.get("prompt_tokens_details"))
.and_then(Value::as_object)
.and_then(|details| {
details
.get("cache_write_tokens")
.or_else(|| details.get("cached_creation_tokens"))
.or_else(|| details.get("cache_creation_tokens"))
})
.and_then(Value::as_u64)
})
.unwrap_or(0);
let cache_read_tokens = usage
.get("cache_read_input_tokens")
.and_then(Value::as_u64)
.or_else(|| {
usage
.get("input_tokens_details")
.or_else(|| usage.get("prompt_tokens_details"))
.and_then(Value::as_object)
.and_then(|details| details.get("cached_tokens"))
.and_then(Value::as_u64)
})
.unwrap_or(0);
let reasoning_tokens = usage
.get("reasoning_tokens")
.and_then(Value::as_u64)
.or_else(|| {
usage
.get("output_tokens_details")
.or_else(|| usage.get("completion_tokens_details"))
.and_then(Value::as_object)
.and_then(|details| details.get("reasoning_tokens"))
.and_then(Value::as_u64)
})
.unwrap_or(0);
let total_tokens = usage.get("total_tokens").and_then(Value::as_u64).unwrap_or(
input_tokens
.saturating_add(output_tokens)
.saturating_add(reasoning_tokens),
);
if input_tokens == 0 && total_tokens > output_tokens {
input_tokens = total_tokens.saturating_sub(output_tokens);
}
Some(CanonicalUsage {
input_tokens,
input_tokens_include_cache: cache_read_tokens > 0 || cache_creation_tokens > 0,
output_tokens,
total_tokens,
cache_creation_tokens,
cache_read_tokens,
reasoning_tokens,
..CanonicalUsage::default()
})
}
pub fn openai_stream_payload_is_terminal_error(payload: &Value) -> bool {
let response = payload.get("response").and_then(Value::as_object);
if payload.get("error").is_some_and(|error| !error.is_null())
|| response
.and_then(|response| response.get("error"))
.is_some_and(|error| !error.is_null())
{
return true;
}
let event_type = payload
.get("type")
.and_then(Value::as_str)
.unwrap_or_default();
if matches!(event_type, "error" | "response.failed") {
return true;
}
response
.and_then(|response| response.get("status"))
.and_then(Value::as_str)
.is_some_and(|status| status == "failed")
}
pub fn openai_stream_terminal_error_body(payload: &Value) -> Option<Value> {
if !openai_stream_payload_is_terminal_error(payload) {
return None;
}
let event_type = payload
.get("type")
.and_then(Value::as_str)
.unwrap_or_default();
let response = payload.get("response").and_then(Value::as_object);
let status = response
.and_then(|response| response.get("status"))
.and_then(Value::as_str);
let raw_error = response
.and_then(|response| response.get("error"))
.or_else(|| payload.get("error"));
let mut error = raw_error
.and_then(Value::as_object)
.cloned()
.unwrap_or_default();
let message = error
.get("message")
.and_then(Value::as_str)
.map(ToOwned::to_owned)
.or_else(|| raw_error.and_then(Value::as_str).map(ToOwned::to_owned))
.or_else(|| {
payload
.get("message")
.and_then(Value::as_str)
.map(ToOwned::to_owned)
})
.or_else(|| {
response
.and_then(|response| response.get("incomplete_details"))
.and_then(|details| details.get("reason"))
.and_then(Value::as_str)
.map(|reason| format!("Response incomplete: {reason}"))
})
.or_else(|| status.map(|status| format!("Response ended with status {status}")))
.unwrap_or_else(|| "Upstream stream ended with an error".to_string());
error
.entry("message".to_string())
.or_insert_with(|| Value::String(message));
error.entry("type".to_string()).or_insert_with(|| {
if event_type == "response.incomplete" || status == Some("incomplete") {
Value::String("incomplete".to_string())
} else {
Value::String("server_error".to_string())
}
});
if !error.contains_key("code") {
if let Some(reason) = response
.and_then(|response| response.get("incomplete_details"))
.and_then(|details| details.get("reason"))
.and_then(Value::as_str)
{
error.insert("code".to_string(), Value::String(reason.to_string()));
}
}
Some(json!({ "error": Value::Object(error) }))
}
pub fn openai_stream_terminal_error_message(payload: &Value) -> Option<String> {
openai_stream_terminal_error_body(payload)
.and_then(|body| body.get("error").cloned())
.and_then(|error| {
error
.get("message")
.and_then(Value::as_str)
.map(ToOwned::to_owned)
})
}
pub fn canonical_usage_from_claude_usage(value: Option<&Value>) -> Option<CanonicalUsage> {
let usage = value?.as_object()?;
let input_tokens = usage
.get("input_tokens")
.and_then(Value::as_u64)
.unwrap_or(0);
let output_tokens = usage
.get("output_tokens")
.and_then(Value::as_u64)
.unwrap_or(0);
let cache_creation_ephemeral_5m_tokens = usage
.get("cache_creation")
.and_then(Value::as_object)
.and_then(|value| value.get("ephemeral_5m_input_tokens"))
.and_then(Value::as_u64)
.unwrap_or(0);
let cache_creation_ephemeral_1h_tokens = usage
.get("cache_creation")
.and_then(Value::as_object)
.and_then(|value| value.get("ephemeral_1h_input_tokens"))
.and_then(Value::as_u64)
.unwrap_or(0);
let cache_creation_tokens = usage
.get("cache_creation_input_tokens")
.and_then(Value::as_u64)
.unwrap_or(
cache_creation_ephemeral_5m_tokens.saturating_add(cache_creation_ephemeral_1h_tokens),
);
let cache_read_tokens = usage
.get("cache_read_input_tokens")
.and_then(Value::as_u64)
.unwrap_or(0);
let reasoning_tokens = usage
.get("output_tokens_details")
.and_then(Value::as_object)
.and_then(|details| {
details
.get("thinking_tokens")
.or_else(|| details.get("reasoning_tokens"))
})
.or_else(|| usage.get("reasoning_tokens"))
.and_then(Value::as_u64)
.unwrap_or(0);
Some(CanonicalUsage {
input_tokens,
input_tokens_include_cache: false,
output_tokens,
total_tokens: input_tokens
.saturating_add(output_tokens)
.saturating_add(reasoning_tokens),
cache_creation_tokens,
cache_creation_ephemeral_5m_tokens,
cache_creation_ephemeral_1h_tokens,
cache_read_tokens,
reasoning_tokens,
})
}
pub fn content_part_from_openai_image_generation_item(
item: &Value,
) -> Option<CanonicalContentPart> {
let item = item.as_object()?;
let result = item
.get("result")
.and_then(Value::as_str)
.map(str::trim)
.filter(|value| !value.is_empty());
let url = item
.get("url")
.and_then(Value::as_str)
.map(str::trim)
.filter(|value| !value.is_empty());
let image = if let Some(result) = result {
if result.starts_with("data:image/")
|| result.starts_with("http://")
|| result.starts_with("https://")
{
result.to_string()
} else {
let mime_type = item
.get("mime_type")
.and_then(Value::as_str)
.map(str::trim)
.filter(|value| !value.is_empty())
.map(ToOwned::to_owned)
.or_else(|| {
item.get("output_format")
.and_then(Value::as_str)
.map(openai_image_output_format_to_mime_type)
})
.unwrap_or_else(|| "image/png".to_string());
format!("data:{mime_type};base64,{result}")
}
} else {
url?.to_string()
};
Some(CanonicalContentPart::ImageUrl(image))
}
fn openai_image_output_format_to_mime_type(output_format: &str) -> String {
match output_format.trim().to_ascii_lowercase().as_str() {
"jpeg" | "jpg" => "image/jpeg",
"webp" => "image/webp",
"gif" => "image/gif",
_ => "image/png",
}
.to_string()
}
pub fn canonical_usage_from_gemini_usage(value: Option<&Value>) -> Option<CanonicalUsage> {
let usage = value?.as_object()?;
let input_tokens = usage
.get("promptTokenCount")
.or_else(|| usage.get("prompt_token_count"))
.and_then(Value::as_u64)
.unwrap_or(0);
let output_tokens = usage
.get("candidatesTokenCount")
.or_else(|| usage.get("candidates_token_count"))
.and_then(Value::as_u64)
.unwrap_or(0);
let reasoning_tokens = usage
.get("thoughtsTokenCount")
.or_else(|| usage.get("thoughts_token_count"))
.and_then(Value::as_u64)
.unwrap_or(0);
let cache_read_tokens = usage
.get("cachedContentTokenCount")
.or_else(|| usage.get("cached_content_token_count"))
.and_then(Value::as_u64)
.unwrap_or(0);
let total_tokens = usage
.get("totalTokenCount")
.or_else(|| usage.get("total_token_count"))
.and_then(Value::as_u64)
.unwrap_or(
input_tokens
.saturating_add(output_tokens)
.saturating_add(reasoning_tokens),
);
Some(CanonicalUsage {
input_tokens,
input_tokens_include_cache: cache_read_tokens > 0,
output_tokens: output_tokens.saturating_add(reasoning_tokens),
total_tokens,
cache_read_tokens,
reasoning_tokens,
..CanonicalUsage::default()
})
}
pub fn normalize_openai_finish_reason(value: Option<&str>) -> Option<String> {
match value {
Some("function_call") => Some("tool_calls".to_string()),
Some(other) if !other.trim().is_empty() => Some(other.to_string()),
_ => None,
}
}
pub fn map_openai_finish_reason_to_claude(value: Option<&str>) -> &'static str {
match value {
Some("length") => "max_tokens",
Some("tool_calls") | Some("function_call") => "tool_use",
Some("content_filter") => "content_filtered",
_ => "end_turn",
}
}
pub fn map_openai_finish_reason_to_gemini(value: Option<&str>) -> &'static str {
match value {
Some("length") => "MAX_TOKENS",
Some("content_filter") => "SAFETY",
_ => "STOP",
}
}
pub fn parse_json_arguments_value(arguments: &str) -> Option<Value> {
let trimmed = arguments.trim();
if trimmed.is_empty() {
return Some(Value::Object(Map::new()));
}
serde_json::from_str(trimmed).ok()
}
pub fn build_openai_chat_chunk(
id: &str,
model: &str,
text: String,
tool_calls: Option<Vec<Value>>,
finish_reason: Option<&str>,
) -> Value {
let mut delta = Map::new();
delta.insert("role".to_string(), Value::String("assistant".to_string()));
if !text.is_empty() {
delta.insert("content".to_string(), Value::String(text));
} else if tool_calls.is_none() {
delta.insert("content".to_string(), Value::String(String::new()));
}
if let Some(tool_calls) = tool_calls {
delta.insert("tool_calls".to_string(), Value::Array(tool_calls));
}
json!({
"id": id,
"object": "chat.completion.chunk",
"model": model,
"choices": [{
"index": 0,
"delta": Value::Object(delta),
"finish_reason": finish_reason,
}]
})
}
pub fn build_openai_chat_role_chunk(id: &str, model: &str) -> Value {
json!({
"id": id,
"object": "chat.completion.chunk",
"model": model,
"choices": [{
"index": 0,
"delta": {
"role": "assistant"
},
"finish_reason": Value::Null
}]
})
}
pub fn build_openai_chat_finish_chunk(id: &str, model: &str, finish_reason: Option<&str>) -> Value {
json!({
"id": id,
"object": "chat.completion.chunk",
"model": model,
"choices": [{
"index": 0,
"delta": {},
"finish_reason": finish_reason,
}]
})
}
pub fn build_openai_chat_usage_chunk(
id: &str,
model: &str,
prompt_tokens: u64,
completion_tokens: u64,
total_tokens: u64,
reasoning_tokens: u64,
) -> Value {
build_openai_chat_usage_chunk_with_cache(
id,
model,
prompt_tokens,
completion_tokens,
total_tokens,
reasoning_tokens,
0,
0,
)
}
#[allow(clippy::too_many_arguments)]
pub fn build_openai_chat_usage_chunk_with_cache(
id: &str,
model: &str,
prompt_tokens: u64,
completion_tokens: u64,
total_tokens: u64,
reasoning_tokens: u64,
cache_creation_tokens: u64,
cache_read_tokens: u64,
) -> Value {
let usage = openai_chat_usage_payload(
prompt_tokens,
completion_tokens,
total_tokens,
reasoning_tokens,
cache_creation_tokens,
cache_read_tokens,
);
json!({
"id": id,
"object": "chat.completion.chunk",
"model": model,
"choices": [],
"usage": usage,
})
}
pub fn build_openai_chat_usage_chunk_from_usage(
id: &str,
model: &str,
usage: &CanonicalUsage,
) -> Value {
let input_tokens = inclusive_input_tokens_from_usage(usage);
build_openai_chat_usage_chunk_with_cache(
id,
model,
input_tokens,
usage.output_tokens,
inclusive_total_tokens_from_usage(usage, input_tokens),
usage.reasoning_tokens,
cache_creation_tokens_for_usage(usage),
usage.cache_read_tokens,
)
}
pub fn openai_responses_usage_from_usage(usage: &CanonicalUsage) -> Value {
let mut output = Map::new();
let input_tokens = inclusive_input_tokens_from_usage(usage);
output.insert("input_tokens".to_string(), Value::from(input_tokens));
output.insert(
"output_tokens".to_string(),
Value::from(usage.output_tokens),
);
output.insert(
"total_tokens".to_string(),
Value::from(inclusive_total_tokens_from_usage(usage, input_tokens)),
);
if usage.reasoning_tokens > 0 {
output.insert(
"output_tokens_details".to_string(),
json!({ "reasoning_tokens": usage.reasoning_tokens }),
);
}
insert_openai_token_details(
&mut output,
"input_tokens_details",
cache_creation_tokens_for_usage(usage),
usage.cache_read_tokens,
);
Value::Object(output)
}
pub fn claude_usage_from_usage(usage: &CanonicalUsage) -> Value {
let mut output = Map::new();
output.insert(
"input_tokens".to_string(),
Value::from(claude_input_tokens_from_usage(usage)),
);
output.insert(
"output_tokens".to_string(),
Value::from(usage.output_tokens),
);
if usage.cache_read_tokens > 0 {
output.insert(
"cache_read_input_tokens".to_string(),
Value::from(usage.cache_read_tokens),
);
}
let cache_creation_tokens = cache_creation_tokens_for_usage(usage);
if cache_creation_tokens > 0 {
output.insert(
"cache_creation_input_tokens".to_string(),
Value::from(cache_creation_tokens),
);
}
if usage.cache_creation_ephemeral_5m_tokens > 0 || usage.cache_creation_ephemeral_1h_tokens > 0
{
output.insert(
"cache_creation".to_string(),
json!({
"ephemeral_5m_input_tokens": usage.cache_creation_ephemeral_5m_tokens,
"ephemeral_1h_input_tokens": usage.cache_creation_ephemeral_1h_tokens,
}),
);
}
Value::Object(output)
}
pub fn gemini_usage_metadata_from_usage(usage: &CanonicalUsage) -> Value {
let visible_output_tokens = usage.output_tokens.saturating_sub(usage.reasoning_tokens);
let input_tokens = inclusive_input_tokens_from_usage(usage);
let mut output = Map::new();
output.insert("promptTokenCount".to_string(), Value::from(input_tokens));
output.insert(
"candidatesTokenCount".to_string(),
Value::from(visible_output_tokens),
);
output.insert(
"totalTokenCount".to_string(),
Value::from(inclusive_total_tokens_from_usage(usage, input_tokens)),
);
if usage.reasoning_tokens > 0 {
output.insert(
"thoughtsTokenCount".to_string(),
Value::from(usage.reasoning_tokens),
);
}
if usage.cache_read_tokens > 0 {
output.insert(
"cachedContentTokenCount".to_string(),
Value::from(usage.cache_read_tokens),
);
}
Value::Object(output)
}
fn openai_chat_usage_payload(
prompt_tokens: u64,
completion_tokens: u64,
total_tokens: u64,
reasoning_tokens: u64,
cache_creation_tokens: u64,
cache_read_tokens: u64,
) -> Map<String, Value> {
let mut usage = Map::new();
usage.insert("prompt_tokens".to_string(), Value::from(prompt_tokens));
usage.insert(
"completion_tokens".to_string(),
Value::from(completion_tokens),
);
usage.insert("total_tokens".to_string(), Value::from(total_tokens));
if reasoning_tokens > 0 {
usage.insert(
"completion_tokens_details".to_string(),
json!({ "reasoning_tokens": reasoning_tokens }),
);
}
insert_openai_token_details(
&mut usage,
"prompt_tokens_details",
cache_creation_tokens,
cache_read_tokens,
);
usage
}
fn insert_openai_token_details(
output: &mut Map<String, Value>,
details_key: &str,
cache_creation_tokens: u64,
cache_read_tokens: u64,
) {
if cache_creation_tokens == 0 && cache_read_tokens == 0 {
return;
}
let mut details = Map::new();
if cache_read_tokens > 0 {
details.insert("cached_tokens".to_string(), Value::from(cache_read_tokens));
}
if cache_creation_tokens > 0 {
details.insert(
"cache_write_tokens".to_string(),
Value::from(cache_creation_tokens),
);
}
output.insert(details_key.to_string(), Value::Object(details));
}
fn cache_creation_tokens_for_usage(usage: &CanonicalUsage) -> u64 {
if usage.cache_creation_tokens > 0 {
usage.cache_creation_tokens
} else {
usage
.cache_creation_ephemeral_5m_tokens
.saturating_add(usage.cache_creation_ephemeral_1h_tokens)
}
}
fn cache_input_tokens_for_usage(usage: &CanonicalUsage) -> u64 {
usage
.cache_read_tokens
.saturating_add(cache_creation_tokens_for_usage(usage))
}
fn claude_input_tokens_from_usage(usage: &CanonicalUsage) -> u64 {
if usage.input_tokens_include_cache {
usage
.input_tokens
.saturating_sub(cache_input_tokens_for_usage(usage))
} else {
usage.input_tokens
}
}
fn inclusive_input_tokens_from_usage(usage: &CanonicalUsage) -> u64 {
if usage.input_tokens_include_cache {
usage.input_tokens
} else {
usage
.input_tokens
.saturating_add(cache_input_tokens_for_usage(usage))
}
}
fn inclusive_total_tokens_from_usage(usage: &CanonicalUsage, input_tokens: u64) -> u64 {
if usage.total_tokens > 0
&& (usage.input_tokens_include_cache || cache_input_tokens_for_usage(usage) == 0)
{
usage.total_tokens
} else {
input_tokens.saturating_add(usage.output_tokens)
}
}
#[cfg(test)]
mod tests {
use serde_json::json;
use super::{
openai_stream_payload_is_terminal_error, openai_stream_terminal_error_body,
openai_stream_terminal_error_message,
};
#[test]
fn completed_openai_responses_payload_with_null_error_is_not_terminal_error() {
let payload = json!({
"type": "response.completed",
"response": {
"id": "resp_123",
"object": "response",
"status": "completed",
"error": null,
"incomplete_details": null,
"output": [],
"usage": {
"input_tokens": 1,
"output_tokens": 2,
"total_tokens": 3
}
},
"error": null
});
assert!(!openai_stream_payload_is_terminal_error(&payload));
assert!(openai_stream_terminal_error_body(&payload).is_none());
assert!(openai_stream_terminal_error_message(&payload).is_none());
}
}
File diff suppressed because it is too large Load Diff
@@ -0,0 +1,5 @@
pub mod common;
pub mod format_matrix;
pub use common::{CanonicalStreamEvent, CanonicalStreamFrame};
pub use format_matrix::{StreamingStandardFormatMatrix, StreamingStandardTerminalObserver};
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
@@ -0,0 +1,34 @@
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum LocalVideoCreateFamily {
OpenAi,
Gemini,
}
#[derive(Debug, Clone, Copy)]
pub struct LocalVideoCreateSpec {
pub api_format: &'static str,
pub decision_kind: &'static str,
pub report_kind: &'static str,
pub family: LocalVideoCreateFamily,
}
pub fn resolve_sync_spec(plan_kind: &str) -> Option<LocalVideoCreateSpec> {
crate::formats::openai::video::spec::resolve_sync_spec(plan_kind)
.or_else(|| crate::formats::gemini::video::spec::resolve_sync_spec(plan_kind))
}
#[cfg(test)]
mod tests {
use super::{resolve_sync_spec, LocalVideoCreateFamily};
#[test]
fn resolves_openai_and_gemini_video_create_specs() {
let openai = resolve_sync_spec("openai_video_create_sync").expect("openai spec");
assert_eq!(openai.api_format, "openai:video");
assert_eq!(openai.family, LocalVideoCreateFamily::OpenAi);
let gemini = resolve_sync_spec("gemini_video_create_sync").expect("gemini spec");
assert_eq!(gemini.api_format, "gemini:video");
assert_eq!(gemini.family, LocalVideoCreateFamily::Gemini);
}
}

Some files were not shown because too many files have changed in this diff Show More